Bladeren bron

goctl added

kim 4 jaren geleden
bovenliggende
commit
121323b8c3
100 gewijzigde bestanden met toevoegingen van 8627 en 0 verwijderingen
  1. 4 0
      tools/goctl/Makefile
  2. 78 0
      tools/goctl/api/apigen/gen.go
  3. 26 0
      tools/goctl/api/apigen/util.go
  4. 40 0
      tools/goctl/api/dartgen/gen.go
  5. 75 0
      tools/goctl/api/dartgen/genapi.go
  6. 79 0
      tools/goctl/api/dartgen/gendata.go
  7. 66 0
      tools/goctl/api/dartgen/genvars.go
  8. 118 0
      tools/goctl/api/dartgen/util.go
  9. 133 0
      tools/goctl/api/dartgen/vars.go
  10. 7 0
      tools/goctl/api/demo/config/config.go
  11. 25 0
      tools/goctl/api/demo/demo.go
  12. 8 0
      tools/goctl/api/demo/etc/user.json
  13. 33 0
      tools/goctl/api/demo/handler/getuserhandler.go
  14. 17 0
      tools/goctl/api/demo/handler/handlers.go
  15. 4 0
      tools/goctl/api/demo/svc/servicecontext.go
  16. 82 0
      tools/goctl/api/docgen/doc.go
  17. 65 0
      tools/goctl/api/docgen/gen.go
  18. 114 0
      tools/goctl/api/format/format.go
  19. 134 0
      tools/goctl/api/gogen/gen.go
  20. 48 0
      tools/goctl/api/gogen/genconfig.go
  21. 56 0
      tools/goctl/api/gogen/genetc.go
  22. 199 0
      tools/goctl/api/gogen/genhandlers.go
  23. 130 0
      tools/goctl/api/gogen/genlogic.go
  24. 85 0
      tools/goctl/api/gogen/genmain.go
  25. 193 0
      tools/goctl/api/gogen/genroutes.go
  26. 63 0
      tools/goctl/api/gogen/genservicecontext.go
  27. 146 0
      tools/goctl/api/gogen/gentypes.go
  28. 67 0
      tools/goctl/api/gogen/util.go
  29. 12 0
      tools/goctl/api/gogen/vars.go
  30. 46 0
      tools/goctl/api/javagen/gen.go
  31. 85 0
      tools/goctl/api/javagen/gencomponents.go
  32. 277 0
      tools/goctl/api/javagen/genpacket.go
  33. 163 0
      tools/goctl/api/javagen/util.go
  34. 3 0
      tools/goctl/api/javagen/vars.go
  35. 21 0
      tools/goctl/api/main.go
  36. 182 0
      tools/goctl/api/parser/basestate.go
  37. 20 0
      tools/goctl/api/parser/basestate_test.go
  38. 132 0
      tools/goctl/api/parser/entity.go
  39. 62 0
      tools/goctl/api/parser/infostate.go
  40. 57 0
      tools/goctl/api/parser/parser.go
  41. 109 0
      tools/goctl/api/parser/rootstate.go
  42. 97 0
      tools/goctl/api/parser/servicestate.go
  43. 7 0
      tools/goctl/api/parser/state.go
  44. 329 0
      tools/goctl/api/parser/typeparser.go
  45. 95 0
      tools/goctl/api/parser/typestate.go
  46. 103 0
      tools/goctl/api/parser/util.go
  47. 54 0
      tools/goctl/api/parser/validator.go
  48. 16 0
      tools/goctl/api/parser/vars.go
  49. 143 0
      tools/goctl/api/spec/fn.go
  50. 131 0
      tools/goctl/api/spec/spec.go
  51. 44 0
      tools/goctl/api/tsgen/gen.go
  52. 79 0
      tools/goctl/api/tsgen/gencomponents.go
  53. 214 0
      tools/goctl/api/tsgen/genpacket.go
  54. 167 0
      tools/goctl/api/tsgen/util.go
  55. 5 0
      tools/goctl/api/tsgen/vars.go
  56. 13 0
      tools/goctl/api/util/annotation.go
  57. 107 0
      tools/goctl/api/util/case.go
  58. 58 0
      tools/goctl/api/util/tag.go
  59. 159 0
      tools/goctl/api/util/types.go
  60. 74 0
      tools/goctl/api/util/util.go
  61. 29 0
      tools/goctl/api/validate/validate.go
  62. 77 0
      tools/goctl/configgen/genconfigjson.go
  63. 23 0
      tools/goctl/docker/docker.go
  64. 30 0
      tools/goctl/example/rec.proto
  65. 20 0
      tools/goctl/feature/feature.go
  66. 36 0
      tools/goctl/gen/dockerfile.go
  67. 52 0
      tools/goctl/gen/makefile.go
  68. 44 0
      tools/goctl/gen/template.go
  69. 369 0
      tools/goctl/goctl.go
  70. 266 0
      tools/goctl/goctl.md
  71. 126 0
      tools/goctl/k8s/apirpc.go
  72. 46 0
      tools/goctl/k8s/job.go
  73. 136 0
      tools/goctl/k8s/k8s.go
  74. 68 0
      tools/goctl/k8s/rmqsync.go
  75. 33 0
      tools/goctl/model/mongomodel/gen/genmethod.go
  76. 156 0
      tools/goctl/model/mongomodel/gen/genmongomodel.go
  77. 67 0
      tools/goctl/model/mongomodel/gen/genmongomodelbynetwork.go
  78. 238 0
      tools/goctl/model/mongomodel/gen/templatemodel.go
  79. 30 0
      tools/goctl/model/mongomodel/genmongocmd.go
  80. 167 0
      tools/goctl/model/mongomodel/utils/parsesimplegofile.go
  81. 430 0
      tools/goctl/model/sql/README.MD
  82. 108 0
      tools/goctl/model/sql/gen/convert.go
  83. 51 0
      tools/goctl/model/sql/gen/delete.go
  84. 7 0
      tools/goctl/model/sql/gen/error.go
  85. 39 0
      tools/goctl/model/sql/gen/field.go
  86. 55 0
      tools/goctl/model/sql/gen/findallbyfield.go
  87. 63 0
      tools/goctl/model/sql/gen/findallbylimit.go
  88. 30 0
      tools/goctl/model/sql/gen/findone.go
  89. 67 0
      tools/goctl/model/sql/gen/fineonebyfield.go
  90. 23 0
      tools/goctl/model/sql/gen/imports.go
  91. 37 0
      tools/goctl/model/sql/gen/insert.go
  92. 106 0
      tools/goctl/model/sql/gen/keys.go
  93. 100 0
      tools/goctl/model/sql/gen/keys_test.go
  94. 86 0
      tools/goctl/model/sql/gen/model.go
  95. 24 0
      tools/goctl/model/sql/gen/new.go
  96. 99 0
      tools/goctl/model/sql/gen/shared.go
  97. 26 0
      tools/goctl/model/sql/gen/tag.go
  98. 30 0
      tools/goctl/model/sql/gen/types.go
  99. 38 0
      tools/goctl/model/sql/gen/update.go
  100. 36 0
      tools/goctl/model/sql/gen/vars.go

+ 4 - 0
tools/goctl/Makefile

@@ -0,0 +1,4 @@
+version := $(shell /bin/date "+%Y-%m-%d %H:%M")
+
+build:
+	go build -ldflags="-s -w" -ldflags="-X 'main.BuildTime=$(version)'" goctl.go && upx goctl

+ 78 - 0
tools/goctl/api/apigen/gen.go

@@ -0,0 +1,78 @@
+package apigen
+
+import (
+	"errors"
+	"fmt"
+	"path/filepath"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/util"
+
+	"github.com/logrusorgru/aurora"
+	"github.com/urfave/cli"
+)
+
+const apiTemplate = `info(
+	title: // TODO: add title
+	desc: // TODO: add description
+	author: {{.gitUser}}
+	email: {{.gitEmail}}
+)
+
+type request struct{
+	// TODO: add members here and delete this comment
+}
+
+type response struct{
+	// TODO: add members here and delete this comment
+}
+
+@server(
+    port: // TODO: add port here and delete this comment
+)
+service {{.serviceName}} {
+	@server(
+		handler: // TODO: set handler name and delete this comment
+	)
+	// TODO: edit the below line
+	// get /users/id/:userId(request) returns(response)
+
+	@server(
+		handler: // TODO: set handler name and delete this comment
+	)
+	// TODO: edit the below line
+	// post /users/create(request)
+}
+`
+
+func ApiCommand(c *cli.Context) error {
+	apiFile := c.String("o")
+	if len(apiFile) == 0 {
+		return errors.New("missing -o")
+	}
+
+	fp, err := util.CreateIfNotExist(apiFile)
+	if err != nil {
+		return err
+	}
+	defer fp.Close()
+
+	baseName := util.FileNameWithoutExt(filepath.Base(apiFile))
+	if strings.HasSuffix(strings.ToLower(baseName), "-api") {
+		baseName = baseName[:len(baseName)-4]
+	} else if strings.HasSuffix(strings.ToLower(baseName), "api") {
+		baseName = baseName[:len(baseName)-3]
+	}
+	t := template.Must(template.New("etcTemplate").Parse(apiTemplate))
+	if err := t.Execute(fp, map[string]string{
+		"gitUser":     getGitName(),
+		"gitEmail":    getGitEmail(),
+		"serviceName": baseName + "-api",
+	}); err != nil {
+		return err
+	}
+
+	fmt.Println(aurora.Green("Done."))
+	return nil
+}

+ 26 - 0
tools/goctl/api/apigen/util.go

@@ -0,0 +1,26 @@
+package apigen
+
+import (
+	"os/exec"
+	"strings"
+)
+
+func getGitName() string {
+	cmd := exec.Command("git", "config", "user.name")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return ""
+	}
+
+	return strings.TrimSpace(string(out))
+}
+
+func getGitEmail() string {
+	cmd := exec.Command("git", "config", "user.email")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return ""
+	}
+
+	return strings.TrimSpace(string(out))
+}

+ 40 - 0
tools/goctl/api/dartgen/gen.go

@@ -0,0 +1,40 @@
+package dartgen
+
+import (
+	"errors"
+	"strings"
+
+	"zero/core/lang"
+	"zero/tools/goctl/api/parser"
+
+	"github.com/urfave/cli"
+)
+
+func DartCommand(c *cli.Context) error {
+	apiFile := c.String("api")
+	dir := c.String("dir")
+	if len(apiFile) == 0 {
+		return errors.New("missing -api")
+	}
+	if len(dir) == 0 {
+		return errors.New("missing -dir")
+	}
+
+	p, err := parser.NewParser(apiFile)
+	if err != nil {
+		return err
+	}
+	api, err := p.Parse()
+	if err != nil {
+		return err
+	}
+
+	if !strings.HasSuffix(dir, "/") {
+		dir = dir + "/"
+	}
+	api.Info.Title = strings.Replace(apiFile, ".api", "", -1)
+	lang.Must(genData(dir+"data/", api))
+	lang.Must(genApi(dir+"api/", api))
+	lang.Must(genVars(dir + "vars/"))
+	return nil
+}

+ 75 - 0
tools/goctl/api/dartgen/genapi.go

@@ -0,0 +1,75 @@
+package dartgen
+
+import (
+	"os"
+	"text/template"
+
+	"zero/core/logx"
+	"zero/tools/goctl/api/spec"
+)
+
+const apiTemplate = `import 'api.dart';
+import '../data/{{with .Info}}{{.Title}}{{end}}.dart';
+{{with .Service}}
+/// {{.Name}}
+{{range .Routes}}
+/// --{{.Path}}--
+///
+/// 请求: {{with .RequestType}}{{.Name}}{{end}}
+/// 返回: {{with .ResponseType}}{{.Name}}{{end}}
+Future {{pathToFuncName .Path}}( {{if ne .Method "get"}}{{with .RequestType}}{{.Name}} request,{{end}}{{end}}
+    {Function({{with .ResponseType}}{{.Name}}{{end}}) ok,
+    Function(String) fail,
+    Function eventually}) async {
+  await api{{if eq .Method "get"}}Get{{else}}Post{{end}}('{{.Path}}',{{if ne .Method "get"}}request,{{end}}
+  	 ok: (data) {
+    if (ok != null) ok({{with .ResponseType}}{{.Name}}{{end}}.fromJson(data));
+  }, fail: fail, eventually: eventually);
+}
+{{end}}
+{{end}}`
+
+func genApi(dir string, api *spec.ApiSpec) error {
+	e := os.MkdirAll(dir, 0755)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+	e = genApiFile(dir)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+
+	file, e := os.OpenFile(dir+api.Info.Title+".dart", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+	defer file.Close()
+
+	t := template.New("apiTemplate")
+	t = t.Funcs(funcMap)
+	t, e = t.Parse(apiTemplate)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+	t.Execute(file, api)
+	return nil
+}
+
+func genApiFile(dir string) error {
+	path := dir + "api.dart"
+	if fileExists(path) {
+		return nil
+	}
+	apiFile, e := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+	defer apiFile.Close()
+	apiFile.WriteString(apiFileContent)
+	return nil
+}

+ 79 - 0
tools/goctl/api/dartgen/gendata.go

@@ -0,0 +1,79 @@
+package dartgen
+
+import (
+	"os"
+	"text/template"
+
+	"zero/core/logx"
+	"zero/tools/goctl/api/spec"
+)
+
+const dataTemplate = `// --{{with .Info}}{{.Title}}{{end}}--
+{{ range .Types}}
+class {{.Name}}{
+	{{range .Members}}
+	/// {{.Comment}}
+	final {{.Type}} {{lowCamelCase .Name}};
+	{{end}}
+	{{.Name}}({ {{range .Members}}
+		this.{{lowCamelCase .Name}},{{end}}
+	});
+	factory {{.Name}}.fromJson(Map<String,dynamic> m) {
+		return {{.Name}}({{range .Members}}
+			{{lowCamelCase .Name}}: {{if isDirectType .Type}}m['{{tagGet .Tag "json"}}']{{else if isClassListType .Type}}(m['{{tagGet .Tag "json"}}'] as List<dynamic>).map((i) => {{getCoreType .Type}}.fromJson(i)){{else}}{{.Type}}.fromJson(m['{{tagGet .Tag "json"}}']){{end}},{{end}}
+		);
+	}
+	Map<String,dynamic> toJson() {
+		return { {{range .Members}}
+			'{{tagGet .Tag "json"}}': {{if isDirectType .Type}}{{lowCamelCase .Name}}{{else if isClassListType .Type}}{{lowCamelCase .Name}}.map((i) => i.toJson()){{else}}{{lowCamelCase .Name}}.toJson(){{end}},{{end}}
+		};
+	}
+}
+{{end}}
+`
+
+func genData(dir string, api *spec.ApiSpec) error {
+	e := os.MkdirAll(dir, 0755)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+	e = genTokens(dir)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+
+	file, e := os.OpenFile(dir+api.Info.Title+".dart", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+	defer file.Close()
+
+	t := template.New("dataTemplate")
+	t = t.Funcs(funcMap)
+	t, e = t.Parse(dataTemplate)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+
+	convertMemberType(api)
+	return t.Execute(file, api)
+}
+
+func genTokens(dir string) error {
+	path := dir + "tokens.dart"
+	if fileExists(path) {
+		return nil
+	}
+	tokensFile, e := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+	defer tokensFile.Close()
+	tokensFile.WriteString(tokensFileContent)
+	return nil
+}

+ 66 - 0
tools/goctl/api/dartgen/genvars.go

@@ -0,0 +1,66 @@
+package dartgen
+
+import (
+	"io/ioutil"
+	"os"
+
+	"zero/core/logx"
+)
+
+func genVars(dir string) error {
+	e := os.MkdirAll(dir, 0755)
+	if e != nil {
+		logx.Error(e)
+		return e
+	}
+
+	if !fileExists(dir + "vars.dart") {
+		e = ioutil.WriteFile(dir+"vars.dart", []byte(`const serverHost='demo-crm.xiaoheiban.cn';`), 0644)
+		if e != nil {
+			logx.Error(e)
+			return e
+		}
+	}
+
+	if !fileExists(dir + "kv.dart") {
+		e = ioutil.WriteFile(dir+"kv.dart", []byte(`import 'dart:convert';
+import 'package:shared_preferences/shared_preferences.dart';
+import '../data/tokens.dart';
+
+/// 保存tokens到本地
+///
+/// 传入null则删除本地tokens
+/// 返回:true:设置成功  false:设置失败
+Future<bool> setTokens(Tokens tokens) async {
+  var sp = await SharedPreferences.getInstance();
+  if (tokens == null) {
+    sp.remove('tokens');
+    return true;
+  }
+  return await sp.setString('tokens', jsonEncode(tokens.toJson()));
+}
+
+/// 获取本地存储的tokens
+///
+/// 如果没有,则返回null
+Future<Tokens> getTokens() async {
+  try {
+    var sp = await SharedPreferences.getInstance();
+    var str = sp.getString('tokens');
+    if (str.isEmpty) {
+      return null;
+    }
+    return Tokens.fromJson(jsonDecode(str));
+  } catch (e) {
+    print(e);
+    return null;
+  }
+}
+`), 0644)
+		if e != nil {
+			logx.Error(e)
+			return e
+		}
+	}
+	return nil
+}

+ 118 - 0
tools/goctl/api/dartgen/util.go

@@ -0,0 +1,118 @@
+package dartgen
+
+import (
+	"log"
+	"os"
+	"reflect"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/api/util"
+)
+
+func lowCamelCase(s string) string {
+	if len(s) < 1 {
+		return ""
+	}
+	s = util.ToCamelCase(util.ToSnakeCase(s))
+	return util.ToLower(s[:1]) + s[1:]
+}
+
+func pathToFuncName(path string) string {
+	if !strings.HasPrefix(path, "/") {
+		path = "/" + path
+	}
+	if !strings.HasPrefix(path, "/api") {
+		path = "/api" + path
+	}
+
+	path = strings.Replace(path, "/", "_", -1)
+	path = strings.Replace(path, "-", "_", -1)
+
+	camel := util.ToCamelCase(path)
+	return util.ToLower(camel[:1]) + camel[1:]
+}
+
+func tagGet(tag, k string) (reflect.Value, error) {
+	v, _ := util.TagLookup(tag, k)
+	out := strings.Split(v, ",")[0]
+	return reflect.ValueOf(out), nil
+}
+
+func convertMemberType(api *spec.ApiSpec) {
+	for i, t := range api.Types {
+		for j, mem := range t.Members {
+			api.Types[i].Members[j].Type = goTypeToDart(mem.Type)
+		}
+	}
+}
+
+func goTypeToDart(t string) string {
+	t = strings.Replace(t, "*", "", -1)
+	if strings.HasPrefix(t, "[]") {
+		return "List<" + goTypeToDart(t[2:]) + ">"
+	}
+
+	if strings.HasPrefix(t, "map") {
+		tys, e := util.DecomposeType(t)
+		if e != nil {
+			log.Fatal(e)
+		}
+
+		if len(tys) != 2 {
+			log.Fatal("Map type number !=2")
+		}
+
+		return "Map<String," + goTypeToDart(tys[1]) + ">"
+	}
+
+	switch t {
+	case "string":
+		return "String"
+	case "int", "int32", "int64":
+		return "int"
+	case "float", "float32", "float64":
+		return "double"
+	case "bool":
+		return "bool"
+	default:
+		return t
+	}
+}
+
+func isDirectType(s string) bool {
+	return isAtomicType(s) || isListType(s) && isAtomicType(getCoreType(s))
+}
+
+func isAtomicType(s string) bool {
+	switch s {
+	case "String", "int", "double", "bool":
+		return true
+	default:
+		return false
+	}
+}
+
+func isListType(s string) bool {
+	return strings.HasPrefix(s, "List<")
+}
+
+func isClassListType(s string) bool {
+	return strings.HasPrefix(s, "List<") && !isAtomicType(getCoreType(s))
+}
+
+func getCoreType(s string) string {
+	if isAtomicType(s) {
+		return s
+	}
+	if isListType(s) {
+		s = strings.Replace(s, "List<", "", -1)
+		return strings.Replace(s, ">", "", -1)
+	}
+	return s
+}
+
+func fileExists(path string) bool {
+	_, err := os.Stat(path)
+	return !os.IsNotExist(err)
+}

+ 133 - 0
tools/goctl/api/dartgen/vars.go

@@ -0,0 +1,133 @@
+package dartgen
+
+import "text/template"
+
+var funcMap = template.FuncMap{
+	"tagGet":          tagGet,
+	"isDirectType":    isDirectType,
+	"isClassListType": isClassListType,
+	"getCoreType":     getCoreType,
+	"pathToFuncName":  pathToFuncName,
+	"lowCamelCase":    lowCamelCase,
+}
+
+const apiFileContent = `import 'dart:io';
+import 'dart:convert';
+import '../vars/kv.dart';
+import '../vars/vars.dart';
+
+/// 发送POST请求.
+///
+/// data:为你要post的结构体,我们会帮你转换成json字符串;
+/// ok函数:请求成功的时候调用,fail函数:请求失败的时候会调用,eventually函数:无论成功失败都会调用
+Future apiPost(String path, dynamic data,
+    {Map<String, String> header,
+    Function(Map<String, dynamic>) ok,
+    Function(String) fail,
+    Function eventually}) async {
+  await _apiRequest('POST', path, data,
+      header: header, ok: ok, fail: fail, eventually: eventually);
+}
+
+/// 发送GET请求.
+///
+/// ok函数:请求成功的时候调用,fail函数:请求失败的时候会调用,eventually函数:无论成功失败都会调用
+Future apiGet(String path,
+    {Map<String, String> header,
+    Function(Map<String, dynamic>) ok,
+    Function(String) fail,
+    Function eventually}) async {
+  await _apiRequest('GET', path, null,
+      header: header, ok: ok, fail: fail, eventually: eventually);
+}
+
+Future _apiRequest(String method, String path, dynamic data,
+    {Map<String, String> header,
+    Function(Map<String, dynamic>) ok,
+    Function(String) fail,
+    Function eventually}) async {
+  var tokens = await getTokens();
+  try {
+    var client = HttpClient();
+    HttpClientRequest r;
+    if (method == 'POST') {
+      r = await client.postUrl(Uri.parse('https://' + serverHost + path));
+    } else {
+      r = await client.getUrl(Uri.parse('https://' + serverHost + path));
+    }
+
+    r.headers.set('Content-Type', 'application/json');
+    if (tokens != null) {
+      r.headers.set('Authorization', tokens.accessToken);
+    }
+    if (header != null) {
+      header.forEach((k, v) {
+        r.headers.set(k, v);
+      });
+    }
+    var strData = '';
+    if (data != null) {
+      strData = jsonEncode(data);
+    }
+    r.write(strData);
+    var rp = await r.close();
+    var body = await rp.transform(utf8.decoder).join();
+    print('${rp.statusCode} - $path');
+    print('-- request --');
+    print(strData);
+    print('-- response --');
+    print('$body \n');
+    if (rp.statusCode == 404) {
+      if (fail != null) fail('404 not found');
+    } else {
+      Map<String, dynamic> base = jsonDecode(body);
+      if (rp.statusCode == 200) {
+        if (base['code'] != 0) {
+          if (fail != null) fail(base['desc']);
+        } else {
+          if (ok != null) ok(base['data']);
+        }
+      } else if (base['code'] != 0) {
+        if (fail != null) fail(base['desc']);
+      }
+    }
+  } catch (e) {
+    if (fail != null) fail(e.toString());
+  }
+  if (eventually != null) eventually();
+}
+`
+const tokensFileContent = `class Tokens {
+  /// 用于访问的token, 每次请求都必须带在Header里面
+  final String accessToken;
+  final int accessExpire;
+
+  /// 用于刷新token
+  final String refreshToken;
+  final int refreshExpire;
+  final int refreshAfter;
+  Tokens(
+      {this.accessToken,
+      this.accessExpire,
+      this.refreshToken,
+      this.refreshExpire,
+      this.refreshAfter});
+  factory Tokens.fromJson(Map<String, dynamic> m) {
+    return Tokens(
+        accessToken: m['access_token'],
+        accessExpire: m['access_expire'],
+        refreshToken: m['refresh_token'],
+        refreshExpire: m['refresh_expire'],
+        refreshAfter: m['refresh_after']);
+  }
+  Map<String, dynamic> toJson() {
+    return {
+      'access_token': accessToken,
+      'access_expire': accessExpire,
+      'refresh_token': refreshToken,
+      'refresh_expire': refreshExpire,
+      'refresh_after': refreshAfter,
+    };
+  }
+}
+`

+ 7 - 0
tools/goctl/api/demo/config/config.go

@@ -0,0 +1,7 @@
+package config
+
+import "zero/ngin"
+
+type Config struct {
+	ngin.NgConf
+}

+ 25 - 0
tools/goctl/api/demo/demo.go

@@ -0,0 +1,25 @@
+package main
+
+import (
+	"flag"
+
+	"zero/core/conf"
+	"zero/ngin"
+	"zero/tools/goctl/api/demo/config"
+	"zero/tools/goctl/api/demo/handler"
+)
+
+var configFile = flag.String("f", "etc/user.json", "the config file")
+
+func main() {
+	flag.Parse()
+
+	var c config.Config
+	conf.MustLoad(*configFile, &c)
+
+	engine := ngin.MustNewEngine(c.NgConf)
+	defer engine.Stop()
+
+	handler.RegisterHandlers(engine)
+	engine.Start()
+}

+ 8 - 0
tools/goctl/api/demo/etc/user.json

@@ -0,0 +1,8 @@
+{
+    "Name": "user",
+    "Host": "127.0.0.1",
+    "Port": 3333,
+    "Log": {
+        "Mode": "console"
+    }
+}

+ 33 - 0
tools/goctl/api/demo/handler/getuserhandler.go

@@ -0,0 +1,33 @@
+package handler
+
+import (
+	"net/http"
+
+	"zero/core/httpx"
+)
+
+type (
+	request struct {
+		User string `form:"user,optional"`
+	}
+
+	response struct {
+		Code  int    `json:"code"`
+		Greet string `json:"greet"`
+		From  string `json:"from,omitempty"`
+	}
+)
+
+func GreetHandler(w http.ResponseWriter, r *http.Request) {
+	var req request
+	err := httpx.Parse(r, &req)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	httpx.OkJson(w, response{
+		Code:  0,
+		Greet: "hello",
+	})
+}

+ 17 - 0
tools/goctl/api/demo/handler/handlers.go

@@ -0,0 +1,17 @@
+package handler
+
+import (
+	"net/http"
+
+	"zero/ngin"
+)
+
+func RegisterHandlers(engine *ngin.Engine) {
+	engine.AddRoutes([]ngin.Route{
+		{
+			Method:  http.MethodGet,
+			Path:    "/",
+			Handler: GreetHandler,
+		},
+	})
+}

+ 4 - 0
tools/goctl/api/demo/svc/servicecontext.go

@@ -0,0 +1,4 @@
+package svc
+
+type ServiceContext struct {
+}

+ 82 - 0
tools/goctl/api/docgen/doc.go

@@ -0,0 +1,82 @@
+package docgen
+
+import (
+	"bytes"
+	"fmt"
+	"html/template"
+	"strconv"
+	"strings"
+
+	"zero/core/stringx"
+	"zero/tools/goctl/api/gogen"
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/api/util"
+)
+
+const (
+	markdownTemplate = `
+### {{.index}}. {{.routeComment}}
+
+1. 路由定义
+
+- Url: {{.uri}}
+- Method: {{.method}}
+- Request: {{.requestType}}
+- Response: {{.responseType}}
+
+
+2. 类型定义 
+
+{{.responseContent}}  
+
+`
+)
+
+func genDoc(api *spec.ApiSpec, dir string, filename string) error {
+	fp, _, err := util.MaybeCreateFile(dir, "", filename)
+	if err != nil {
+		return err
+	}
+	defer fp.Close()
+
+	var builder strings.Builder
+	for index, route := range api.Service.Routes {
+		routeComment, _ := util.GetAnnotationValue(route.Annotations, "doc", "summary")
+		if len(routeComment) == 0 {
+			routeComment = "N/A"
+		}
+
+		responseContent, err := responseBody(api, route)
+		if err != nil {
+			return err
+		}
+
+		t := template.Must(template.New("markdownTemplate").Parse(markdownTemplate))
+		var tmplBytes bytes.Buffer
+		err = t.Execute(&tmplBytes, map[string]string{
+			"index":           strconv.Itoa(index + 1),
+			"routeComment":    routeComment,
+			"method":          strings.ToUpper(route.Method),
+			"uri":             route.Path,
+			"requestType":     "`" + stringx.TakeOne(route.RequestType.Name, "-") + "`",
+			"responseType":    "`" + stringx.TakeOne(route.ResponseType.Name, "-") + "`",
+			"responseContent": responseContent,
+		})
+		if err != nil {
+			return err
+		}
+
+		builder.Write(tmplBytes.Bytes())
+	}
+	_, err = fp.WriteString(strings.Replace(builder.String(), "&#34;", `"`, -1))
+	return err
+}
+
+func responseBody(api *spec.ApiSpec, route spec.Route) (string, error) {
+	tps := util.GetLocalTypes(api, route)
+	value, err := gogen.BuildTypes(tps)
+	if err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("\n\n```golang\n%s\n```\n", value), nil
+}

+ 65 - 0
tools/goctl/api/docgen/gen.go

@@ -0,0 +1,65 @@
+package docgen
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"zero/tools/goctl/api/parser"
+
+	"github.com/urfave/cli"
+)
+
+var docDir = "doc"
+
+func DocCommand(c *cli.Context) error {
+	dir := c.String("dir")
+	if len(dir) == 0 {
+		return errors.New("missing -dir")
+	}
+
+	files, err := filePathWalkDir(dir)
+	if err != nil {
+		return errors.New(fmt.Sprintf("dir %s not exist", dir))
+	}
+
+	err = os.RemoveAll(dir + "/" + docDir + "/")
+	if err != nil {
+		return err
+	}
+	for _, f := range files {
+		p, err := parser.NewParser(f)
+		if err != nil {
+			return errors.New(fmt.Sprintf("parse file: %s, err: %s", f, err.Error()))
+		}
+		api, err := p.Parse()
+		if err != nil {
+			return err
+		}
+		index := strings.Index(f, dir)
+		if index < 0 {
+			continue
+		}
+		dst := dir + "/" + docDir + f[index+len(dir):]
+		index = strings.LastIndex(dst, "/")
+		if index < 0 {
+			continue
+		}
+		dir := dst[:index]
+		genDoc(api, dir, strings.Replace(dst[index+1:], ".api", ".md", 1))
+	}
+	return nil
+}
+
+func filePathWalkDir(root string) ([]string, error) {
+	var files []string
+	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+		if !info.IsDir() && strings.HasSuffix(path, ".api") {
+			files = append(files, path)
+		}
+		return nil
+	})
+	return files, err
+}

+ 114 - 0
tools/goctl/api/format/format.go

@@ -0,0 +1,114 @@
+package format
+
+import (
+	"errors"
+	"fmt"
+	"go/format"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"zero/tools/goctl/api/util"
+
+	"zero/core/errorx"
+	"zero/tools/goctl/api/parser"
+
+	"github.com/urfave/cli"
+)
+
+var (
+	reg = regexp.MustCompile("type (?P<name>.*)[\\s]+{")
+)
+
+func GoFormatApi(c *cli.Context) error {
+	dir := c.String("dir")
+	if len(dir) == 0 {
+		return errors.New("missing -dir")
+	}
+
+	printToConsole := c.Bool("p")
+
+	var be errorx.BatchError
+	err := filepath.Walk(dir, func(path string, fi os.FileInfo, errBack error) (err error) {
+		if strings.HasSuffix(path, ".api") {
+			err := ApiFormat(path, printToConsole)
+			if err != nil {
+				be.Add(util.WrapErr(err, fi.Name()))
+			}
+		}
+		return nil
+	})
+	be.Add(err)
+	if be.NotNil() {
+		errs := be.Err().Error()
+		fmt.Println(errs)
+		os.Exit(1)
+	}
+	return be.Err()
+}
+
+func ApiFormat(path string, printToConsole bool) error {
+	data, err := ioutil.ReadFile(path)
+	if err != nil {
+		return err
+	}
+
+	r := reg.ReplaceAllStringFunc(string(data), func(m string) string {
+		parts := reg.FindStringSubmatch(m)
+		if len(parts) < 2 {
+			return m
+		}
+		if !strings.Contains(m, "struct") {
+			return "type " + parts[1] + " struct {"
+		}
+		return m
+	})
+
+	info, st, service, err := parser.MatchStruct(r)
+	if err != nil {
+		return err
+	}
+	info = strings.TrimSpace(info)
+	if len(service) == 0 || len(st) == 0 {
+		return nil
+	}
+
+	fs, err := format.Source([]byte(strings.TrimSpace(st)))
+	if err != nil {
+		str := err.Error()
+		lineNumber := strings.Index(str, ":")
+		if lineNumber > 0 {
+			ln, err := strconv.ParseInt(str[:lineNumber], 10, 64)
+			if err != nil {
+				return err
+			}
+			pn := 0
+			if len(info) > 0 {
+				pn = countRune(info, '\n') + 1
+			}
+			number := int(ln) + pn + 1
+			return errors.New(fmt.Sprintf("line: %d, %s", number, str[lineNumber+1:]))
+		}
+		return err
+	}
+
+	result := strings.Join([]string{info, string(fs), service}, "\n\n")
+	if printToConsole {
+		_, err := fmt.Print(result)
+		return err
+	}
+	return ioutil.WriteFile(path, []byte(result), os.ModePerm)
+}
+
+func countRune(s string, r rune) int {
+	count := 0
+	for _, c := range s {
+		if c == r {
+			count++
+		}
+	}
+	return count
+}

+ 134 - 0
tools/goctl/api/gogen/gen.go

@@ -0,0 +1,134 @@
+package gogen
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"zero/core/lang"
+	apiformat "zero/tools/goctl/api/format"
+	"zero/tools/goctl/api/parser"
+	apiutil "zero/tools/goctl/api/util"
+	"zero/tools/goctl/util"
+
+	"github.com/logrusorgru/aurora"
+	"github.com/urfave/cli"
+)
+
+const tmpFile = "%s-%d"
+
+var tmpDir = path.Join(os.TempDir(), "goctl")
+
+func GoCommand(c *cli.Context) error {
+	apiFile := c.String("api")
+	dir := c.String("dir")
+	if len(apiFile) == 0 {
+		return errors.New("missing -api")
+	}
+	if len(dir) == 0 {
+		return errors.New("missing -dir")
+	}
+
+	p, err := parser.NewParser(apiFile)
+	if err != nil {
+		return err
+	}
+	api, err := p.Parse()
+	if err != nil {
+		return err
+	}
+
+	lang.Must(util.MkdirIfNotExist(dir))
+	lang.Must(genEtc(dir, api))
+	lang.Must(genConfig(dir, api))
+	lang.Must(genMain(dir, api))
+	lang.Must(genServiceContext(dir, api))
+	lang.Must(genTypes(dir, api))
+	lang.Must(genHandlers(dir, api))
+	lang.Must(genRoutes(dir, api))
+	lang.Must(genLogic(dir, api))
+	// it does not work
+	format(dir)
+
+	if err := backupAndSweep(apiFile); err != nil {
+		return err
+	}
+
+	if err = apiformat.ApiFormat(apiFile, false); err != nil {
+		return err
+	}
+
+	fmt.Println(aurora.Green("Done."))
+	return nil
+}
+
+func backupAndSweep(apiFile string) error {
+	var err error
+	var wg sync.WaitGroup
+
+	wg.Add(2)
+	_ = os.MkdirAll(tmpDir, os.ModePerm)
+
+	go func() {
+		_, fileName := filepath.Split(apiFile)
+		_, e := apiutil.Copy(apiFile, fmt.Sprintf(path.Join(tmpDir, tmpFile), fileName, time.Now().Unix()))
+		if e != nil {
+			err = e
+		}
+		wg.Done()
+	}()
+	go func() {
+		if e := sweep(); e != nil {
+			err = e
+		}
+		wg.Done()
+	}()
+	wg.Wait()
+
+	return err
+}
+
+func format(dir string) {
+	cmd := exec.Command("go", "fmt", "./"+dir+"...")
+	_, err := cmd.CombinedOutput()
+	if err != nil {
+		print(err.Error())
+	}
+}
+
+func sweep() error {
+	keepTime := time.Now().AddDate(0, 0, -7)
+	return filepath.Walk(tmpDir, func(fpath string, info os.FileInfo, err error) error {
+		if info.IsDir() {
+			return nil
+		}
+
+		pos := strings.LastIndexByte(info.Name(), '-')
+		if pos > 0 {
+			timestamp := info.Name()[pos+1:]
+			seconds, err := strconv.ParseInt(timestamp, 10, 64)
+			if err != nil {
+				// print error and ignore
+				fmt.Println(aurora.Red(fmt.Sprintf("sweep ignored file: %s", fpath)))
+				return nil
+			}
+
+			tm := time.Unix(seconds, 0)
+			if tm.Before(keepTime) {
+				if err := os.Remove(fpath); err != nil {
+					fmt.Println(aurora.Red(fmt.Sprintf("failed to remove file: %s", fpath)))
+					return err
+				}
+			}
+		}
+
+		return nil
+	})
+}

+ 48 - 0
tools/goctl/api/gogen/genconfig.go

@@ -0,0 +1,48 @@
+package gogen
+
+import (
+	"bytes"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/api/util"
+)
+
+const (
+	configFile     = "config.go"
+	configTemplate = `package config
+
+import (
+	"zero/ngin"
+	{{.authImport}}
+)
+
+type Config struct {
+	ngin.NgConf
+}
+`
+)
+
+func genConfig(dir string, api *spec.ApiSpec) error {
+	fp, created, err := util.MaybeCreateFile(dir, configDir, configFile)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	var authImportStr = ""
+	t := template.Must(template.New("configTemplate").Parse(configTemplate))
+	buffer := new(bytes.Buffer)
+	err = t.Execute(buffer, map[string]string{
+		"authImport": authImportStr,
+	})
+	if err != nil {
+		return nil
+	}
+	formatCode := formatCode(buffer.String())
+	_, err = fp.WriteString(formatCode)
+	return err
+}

+ 56 - 0
tools/goctl/api/gogen/genetc.go

@@ -0,0 +1,56 @@
+package gogen
+
+import (
+	"bytes"
+	"fmt"
+	"strconv"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/api/util"
+)
+
+const (
+	defaultPort = 8888
+	etcDir      = "etc"
+	etcTemplate = `{
+    "Name": "{{.serviceName}}",
+    "Host": "{{.host}}",
+    "Port": {{.port}}
+}`
+)
+
+func genEtc(dir string, api *spec.ApiSpec) error {
+	fp, created, err := util.MaybeCreateFile(dir, etcDir, fmt.Sprintf("%s.json", api.Service.Name))
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	service := api.Service
+	host, ok := util.GetAnnotationValue(service.Annotations, "server", "host")
+	if !ok {
+		host = "0.0.0.0"
+	}
+	port, ok := util.GetAnnotationValue(service.Annotations, "server", "port")
+	if !ok {
+		port = strconv.Itoa(defaultPort)
+	}
+
+	t := template.Must(template.New("etcTemplate").Parse(etcTemplate))
+	buffer := new(bytes.Buffer)
+	err = t.Execute(buffer, map[string]string{
+		"serviceName": service.Name,
+		"host":        host,
+		"port":        port,
+	})
+	if err != nil {
+		return err
+	}
+	formatCode := formatCode(buffer.String())
+	_, err = fp.WriteString(formatCode)
+	return err
+}

+ 199 - 0
tools/goctl/api/gogen/genhandlers.go

@@ -0,0 +1,199 @@
+package gogen
+
+import (
+	"bytes"
+	"fmt"
+	"path"
+	"sort"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	apiutil "zero/tools/goctl/api/util"
+	"zero/tools/goctl/util"
+)
+
+const (
+	handlerTemplate = `package handler
+
+import (
+	"net/http"
+
+	{{.importPackages}}
+)
+
+func {{.handlerName}}(ctx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		l := logic.{{.logic}}(r.Context(), ctx)
+		{{.handlerBody}}
+	}
+}
+`
+	handlerBodyTemplate = `{{.parseRequest}}
+		{{.processBody}}
+`
+	parseRequestTemplate = `var req {{.requestType}}
+		if err := httpx.Parse(r, &req); err != nil {
+			logx.Error(err)
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+`
+	hasRespTemplate = `
+		{{.logicResponse}} l.{{.callee}}({{.req}})
+		// TODO write data to response
+	`
+)
+
+func genHandler(dir string, group spec.Group, route spec.Route) error {
+	handler, ok := apiutil.GetAnnotationValue(route.Annotations, "server", "handler")
+	if !ok {
+		return fmt.Errorf("missing handler annotation for %q", route.Path)
+	}
+	handler = getHandlerName(handler)
+	var reqBody string
+	if len(route.RequestType.Name) > 0 {
+		var bodyBuilder strings.Builder
+		t := template.Must(template.New("parseRequest").Parse(parseRequestTemplate))
+		if err := t.Execute(&bodyBuilder, map[string]string{
+			"requestType": typesPacket + "." + util.Title(route.RequestType.Name),
+		}); err != nil {
+			return err
+		}
+		reqBody = bodyBuilder.String()
+	}
+
+	var req = "req"
+	if len(route.RequestType.Name) == 0 {
+		req = ""
+	}
+	var logicResponse = ""
+	var writeResponse = "nil, nil"
+	if len(route.ResponseType.Name) > 0 {
+		logicResponse = "resp, err :="
+		writeResponse = "resp, err"
+	} else {
+		logicResponse = "err :="
+		writeResponse = "nil, err"
+	}
+	var logicBodyBuilder strings.Builder
+	t := template.Must(template.New("hasRespTemplate").Parse(hasRespTemplate))
+	if err := t.Execute(&logicBodyBuilder, map[string]string{
+		"callee":        strings.Title(strings.TrimSuffix(handler, "Handler")),
+		"req":           req,
+		"logicResponse": logicResponse,
+		"writeResponse": writeResponse,
+	}); err != nil {
+		return err
+	}
+	respBody := logicBodyBuilder.String()
+
+	if !strings.HasSuffix(handler, "Handler") {
+		handler = handler + "Handler"
+	}
+
+	var bodyBuilder strings.Builder
+	bodyTemplate := template.Must(template.New("handlerBodyTemplate").Parse(handlerBodyTemplate))
+	if err := bodyTemplate.Execute(&bodyBuilder, map[string]string{
+		"parseRequest": reqBody,
+		"processBody":  respBody,
+	}); err != nil {
+		return err
+	}
+	return doGenToFile(dir, handler, group, route, bodyBuilder)
+}
+
+func doGenToFile(dir, handler string, group spec.Group, route spec.Route, bodyBuilder strings.Builder) error {
+	if getHandlerFolderPath(group, route) != handlerDir {
+		handler = strings.Title(handler)
+	}
+	parentPkg, err := getParentPackage(dir)
+	if err != nil {
+		return err
+	}
+	filename := strings.ToLower(handler)
+	if strings.HasSuffix(filename, "handler") {
+		filename = filename + ".go"
+	} else {
+		filename = filename + "handler.go"
+	}
+	fp, created, err := apiutil.MaybeCreateFile(dir, getHandlerFolderPath(group, route), filename)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+	t := template.Must(template.New("handlerTemplate").Parse(handlerTemplate))
+	buffer := new(bytes.Buffer)
+	err = t.Execute(buffer, map[string]string{
+		"logic":          "New" + strings.TrimSuffix(strings.Title(handler), "Handler") + "Logic",
+		"importPackages": genHandlerImports(group, route, parentPkg),
+		"handlerName":    handler,
+		"handlerBody":    strings.TrimSpace(bodyBuilder.String()),
+	})
+	if err != nil {
+		return nil
+	}
+	formatCode := formatCode(buffer.String())
+	_, err = fp.WriteString(formatCode)
+	return err
+}
+
+func genHandlers(dir string, api *spec.ApiSpec) error {
+	for _, group := range api.Service.Groups {
+		for _, route := range group.Routes {
+			if err := genHandler(dir, group, route); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func genHandlerImports(group spec.Group, route spec.Route, parentPkg string) string {
+	var imports []string
+	if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
+		imports = append(imports, "\"zero/core/httpx\"")
+	}
+	if len(route.RequestType.Name) > 0 {
+		imports = append(imports, "\"zero/core/logx\"")
+	}
+	imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
+	if len(route.RequestType.Name) > 0 || len(route.ResponseType.Name) > 0 {
+		imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, typesDir)))
+	}
+	imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, getLogicFolderPath(group, route))))
+	sort.Strings(imports)
+
+	return strings.Join(imports, "\n\t")
+}
+
+func getHandlerBaseName(handler string) string {
+	handlerName := util.Untitle(handler)
+	if strings.HasSuffix(handlerName, "handler") {
+		handlerName = strings.ReplaceAll(handlerName, "handler", "")
+	} else if strings.HasSuffix(handlerName, "Handler") {
+		handlerName = strings.ReplaceAll(handlerName, "Handler", "")
+	}
+	return handlerName
+}
+
+func getHandlerFolderPath(group spec.Group, route spec.Route) string {
+	folder, ok := apiutil.GetAnnotationValue(route.Annotations, "server", folderProperty)
+	if !ok {
+		folder, ok = apiutil.GetAnnotationValue(group.Annotations, "server", folderProperty)
+		if !ok {
+			return handlerDir
+		}
+	}
+	folder = strings.TrimPrefix(folder, "/")
+	folder = strings.TrimSuffix(folder, "/")
+	return path.Join(handlerDir, folder)
+}
+
+func getHandlerName(handler string) string {
+	return getHandlerBaseName(handler) + "Handler"
+}

+ 130 - 0
tools/goctl/api/gogen/genlogic.go

@@ -0,0 +1,130 @@
+package gogen
+
+import (
+	"bytes"
+	"fmt"
+	"path"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/api/util"
+)
+
+const logicTemplate = `package logic
+
+import (
+	{{.imports}}
+)
+
+type {{.logic}} struct {
+	ctx context.Context
+	logx.Logger
+}
+
+func New{{.logic}}(ctx context.Context, svcCtx *svc.ServiceContext) {{.logic}} {
+	return {{.logic}}{
+		ctx:    ctx,
+		Logger: logx.WithContext(ctx),
+	}
+	// TODO need set model here from svc
+}
+
+func (l *{{.logic}}) {{.function}}({{.request}}) {{.responseType}} {
+	{{.returnString}}
+}
+
+`
+
+func genLogic(dir string, api *spec.ApiSpec) error {
+	for _, g := range api.Service.Groups {
+		for _, r := range g.Routes {
+			err := genLogicByRoute(dir, g, r)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func genLogicByRoute(dir string, group spec.Group, route spec.Route) error {
+	handler, ok := util.GetAnnotationValue(route.Annotations, "server", "handler")
+	if !ok {
+		return fmt.Errorf("missing handler annotation for %q", route.Path)
+	}
+	handler = strings.TrimSuffix(handler, "handler")
+	handler = strings.TrimSuffix(handler, "Handler")
+	filename := strings.ToLower(handler)
+	goFile := filename + "logic.go"
+	fp, created, err := util.MaybeCreateFile(dir, getLogicFolderPath(group, route), goFile)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	parentPkg, err := getParentPackage(dir)
+	if err != nil {
+		return err
+	}
+	imports := genLogicImports(route, parentPkg)
+
+	responseString := ""
+	returnString := ""
+	requestString := ""
+	if len(route.ResponseType.Name) > 0 {
+		responseString = "(*types." + strings.Title(route.ResponseType.Name) + ", error)"
+		returnString = "return nil, nil"
+	} else {
+		responseString = "error"
+		returnString = "return nil"
+	}
+	if len(route.RequestType.Name) > 0 {
+		requestString = "req " + "types." + strings.Title(route.RequestType.Name)
+	}
+
+	t := template.Must(template.New("logicTemplate").Parse(logicTemplate))
+	buffer := new(bytes.Buffer)
+	err = t.Execute(fp, map[string]string{
+		"imports":      imports,
+		"logic":        strings.Title(handler) + "Logic",
+		"function":     strings.Title(strings.TrimSuffix(handler, "Handler")),
+		"responseType": responseString,
+		"returnString": returnString,
+		"request":      requestString,
+	})
+	if err != nil {
+		return nil
+	}
+	formatCode := formatCode(buffer.String())
+	_, err = fp.WriteString(formatCode)
+	return err
+}
+
+func getLogicFolderPath(group spec.Group, route spec.Route) string {
+	folder, ok := util.GetAnnotationValue(route.Annotations, "server", folderProperty)
+	if !ok {
+		folder, ok = util.GetAnnotationValue(group.Annotations, "server", folderProperty)
+		if !ok {
+			return logicDir
+		}
+	}
+	folder = strings.TrimPrefix(folder, "/")
+	folder = strings.TrimSuffix(folder, "/")
+	return path.Join(logicDir, folder)
+}
+
+func genLogicImports(route spec.Route, parentPkg string) string {
+	var imports []string
+	imports = append(imports, `"context"`)
+	imports = append(imports, "\n")
+	imports = append(imports, `"zero/core/logx"`)
+	if len(route.ResponseType.Name) > 0 || len(route.RequestType.Name) > 0 {
+		imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, typesDir)))
+	}
+	imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
+	return strings.Join(imports, "\n\t")
+}

+ 85 - 0
tools/goctl/api/gogen/genmain.go

@@ -0,0 +1,85 @@
+package gogen
+
+import (
+	"bytes"
+	"fmt"
+	"path"
+	"sort"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/api/util"
+)
+
+const mainTemplate = `package main
+
+import (
+	"flag"
+
+	{{.importPackages}}
+)
+
+var configFile = flag.String("f", "etc/{{.serviceName}}.json", "the config file")
+
+func main() {
+	flag.Parse()
+
+	var c config.Config
+	conf.MustLoad(*configFile, &c)
+
+	ctx := svc.NewServiceContext(c)
+
+	engine := ngin.MustNewEngine(c.NgConf)
+	defer engine.Stop()
+
+	handler.RegisterHandlers(engine, ctx)
+	engine.Start()
+}
+`
+
+func genMain(dir string, api *spec.ApiSpec) error {
+	name := strings.ToLower(api.Service.Name)
+	if strings.HasSuffix(name, "-api") {
+		name = strings.ReplaceAll(name, "-api", "")
+	}
+	goFile := name + ".go"
+	fp, created, err := util.MaybeCreateFile(dir, "", goFile)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	parentPkg, err := getParentPackage(dir)
+	if err != nil {
+		return err
+	}
+
+	t := template.Must(template.New("mainTemplate").Parse(mainTemplate))
+	buffer := new(bytes.Buffer)
+	err = t.Execute(buffer, map[string]string{
+		"importPackages": genMainImports(parentPkg),
+		"serviceName":    api.Service.Name,
+	})
+	if err != nil {
+		return nil
+	}
+	formatCode := formatCode(buffer.String())
+	_, err = fp.WriteString(formatCode)
+	return err
+}
+
+func genMainImports(parentPkg string) string {
+	imports := []string{
+		`"zero/core/conf"`,
+		`"zero/ngin"`,
+	}
+	imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, configDir)))
+	imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, handlerDir)))
+	imports = append(imports, fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
+	sort.Strings(imports)
+	return strings.Join(imports, "\n\t")
+}

+ 193 - 0
tools/goctl/api/gogen/genroutes.go

@@ -0,0 +1,193 @@
+package gogen
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"path"
+	"sort"
+	"strings"
+	"text/template"
+
+	"zero/core/collection"
+	"zero/tools/goctl/api/spec"
+	apiutil "zero/tools/goctl/api/util"
+	"zero/tools/goctl/util"
+)
+
+const (
+	routesFilename = "routes.go"
+	routesTemplate = `// DO NOT EDIT, generated by goctl
+package handler
+
+import (
+	"net/http"
+
+	{{.importPackages}}
+)
+
+func RegisterHandlers(engine *ngin.Engine, serverCtx *svc.ServiceContext) {
+	{{.routesAdditions}}
+}
+`
+	routesAdditionTemplate = `
+	engine.AddRoutes([]ngin.Route{
+		{{.routes}}
+	}{{.jwt}}{{.signature}})
+`
+)
+
+var mapping = map[string]string{
+	"delete": "http.MethodDelete",
+	"get":    "http.MethodGet",
+	"head":   "http.MethodHead",
+	"post":   "http.MethodPost",
+	"put":    "http.MethodPut",
+}
+
+type (
+	group struct {
+		routes           []route
+		jwtEnabled       bool
+		signatureEnabled bool
+		authName         string
+	}
+	route struct {
+		method  string
+		path    string
+		handler string
+	}
+)
+
+func genRoutes(dir string, api *spec.ApiSpec) error {
+	var builder strings.Builder
+	groups, err := getRoutes(api)
+	if err != nil {
+		return err
+	}
+
+	gt := template.Must(template.New("groupTemplate").Parse(routesAdditionTemplate))
+	for _, g := range groups {
+		var gbuilder strings.Builder
+		for _, r := range g.routes {
+			fmt.Fprintf(&gbuilder, `
+		{
+			Method:  %s,
+			Path:    "%s",
+			Handler: %s,
+		},`,
+				r.method, r.path, r.handler)
+		}
+		jwt := ""
+		if g.jwtEnabled {
+			jwt = fmt.Sprintf(", ngin.WithJwt(serverCtx.Config.%s.AccessSecret)", g.authName)
+		}
+		signature := ""
+		if g.signatureEnabled {
+			signature = fmt.Sprintf(", ngin.WithSignature(serverCtx.Config.%s.Signature)", g.authName)
+		}
+		if err := gt.Execute(&builder, map[string]string{
+			"routes":    strings.TrimSpace(gbuilder.String()),
+			"jwt":       jwt,
+			"signature": signature,
+		}); err != nil {
+			return err
+		}
+	}
+
+	parentPkg, err := getParentPackage(dir)
+	if err != nil {
+		return err
+	}
+
+	filename := path.Join(dir, handlerDir, routesFilename)
+	if err := util.RemoveOrQuit(filename); err != nil {
+		return err
+	}
+
+	fp, created, err := apiutil.MaybeCreateFile(dir, handlerDir, routesFilename)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	t := template.Must(template.New("routesTemplate").Parse(routesTemplate))
+	buffer := new(bytes.Buffer)
+	err = t.Execute(buffer, map[string]string{
+		"importPackages":  genRouteImports(parentPkg, api),
+		"routesAdditions": strings.TrimSpace(builder.String()),
+	})
+	if err != nil {
+		return nil
+	}
+	formatCode := formatCode(buffer.String())
+	_, err = fp.WriteString(formatCode)
+	return err
+}
+
+func genRouteImports(parentPkg string, api *spec.ApiSpec) string {
+	var importSet = collection.NewSet()
+	importSet.AddStr(`"zero/ngin"`)
+	importSet.AddStr(fmt.Sprintf("\"%s\"", path.Join(parentPkg, contextDir)))
+	for _, group := range api.Service.Groups {
+		for _, route := range group.Routes {
+			folder, ok := apiutil.GetAnnotationValue(route.Annotations, "server", folderProperty)
+			if !ok {
+				folder, ok = apiutil.GetAnnotationValue(group.Annotations, "server", folderProperty)
+				if !ok {
+					continue
+				}
+			}
+			importSet.AddStr(fmt.Sprintf("%s \"%s\"", folder, path.Join(parentPkg, handlerDir, folder)))
+		}
+	}
+	imports := importSet.KeysStr()
+	sort.Strings(imports)
+	return strings.Join(imports, "\n\t")
+}
+
+func getRoutes(api *spec.ApiSpec) ([]group, error) {
+	var routes []group
+
+	for _, g := range api.Service.Groups {
+		var groupedRoutes group
+		for _, r := range g.Routes {
+			handler, ok := apiutil.GetAnnotationValue(r.Annotations, "server", "handler")
+			if !ok {
+				return nil, fmt.Errorf("missing handler annotation for route %q", r.Path)
+			}
+			handler = getHandlerBaseName(handler) + "Handler(serverCtx)"
+			folder, ok := apiutil.GetAnnotationValue(r.Annotations, "server", folderProperty)
+			if ok {
+				handler = folder + "." + strings.ToUpper(handler[:1]) + handler[1:]
+			} else {
+				folder, ok = apiutil.GetAnnotationValue(g.Annotations, "server", folderProperty)
+				if ok {
+					handler = folder + "." + strings.ToUpper(handler[:1]) + handler[1:]
+				}
+			}
+			groupedRoutes.routes = append(groupedRoutes.routes, route{
+				method:  mapping[r.Method],
+				path:    r.Path,
+				handler: handler,
+			})
+		}
+
+		if value, ok := apiutil.GetAnnotationValue(g.Annotations, "server", "jwt"); ok {
+			groupedRoutes.authName = value
+			groupedRoutes.jwtEnabled = true
+		}
+		if value, ok := apiutil.GetAnnotationValue(g.Annotations, "server", "signature"); ok {
+			if groupedRoutes.authName != "" && groupedRoutes.authName != value {
+				return nil, errors.New("auth signature should config same")
+			}
+			groupedRoutes.signatureEnabled = true
+		}
+		routes = append(routes, groupedRoutes)
+	}
+
+	return routes, nil
+}

+ 63 - 0
tools/goctl/api/gogen/genservicecontext.go

@@ -0,0 +1,63 @@
+package gogen
+
+import (
+	"bytes"
+	"fmt"
+	"path"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/api/util"
+)
+
+const (
+	contextFilename = "servicecontext.go"
+	contextTemplate = `package svc
+
+import {{.configImport}}
+
+type ServiceContext struct {
+	Config {{.config}}
+}
+
+func NewServiceContext(config {{.config}}) *ServiceContext {
+	return &ServiceContext{Config: config}
+}
+
+`
+)
+
+func genServiceContext(dir string, api *spec.ApiSpec) error {
+	fp, created, err := util.MaybeCreateFile(dir, contextDir, contextFilename)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	var authNames = getAuths(api)
+	var auths []string
+	for _, item := range authNames {
+		auths = append(auths, fmt.Sprintf("%s config.AuthConfig", item))
+	}
+
+	parentPkg, err := getParentPackage(dir)
+	if err != nil {
+		return err
+	}
+	var configImport = "\"" + path.Join(parentPkg, configDir) + "\""
+	t := template.Must(template.New("contextTemplate").Parse(contextTemplate))
+	buffer := new(bytes.Buffer)
+	err = t.Execute(buffer, map[string]string{
+		"configImport": configImport,
+		"config":       "config.Config",
+	})
+	if err != nil {
+		return nil
+	}
+	formatCode := formatCode(buffer.String())
+	_, err = fp.WriteString(formatCode)
+	return err
+}

+ 146 - 0
tools/goctl/api/gogen/gentypes.go

@@ -0,0 +1,146 @@
+package gogen
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"path"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	apiutil "zero/tools/goctl/api/util"
+	"zero/tools/goctl/util"
+)
+
+const (
+	typesFile     = "types.go"
+	typesTemplate = `// DO NOT EDIT, generated by goctl
+package types{{if .containsTime}}
+import (
+	"time"
+){{end}}
+{{.types}}
+`
+)
+
+func BuildTypes(types []spec.Type) (string, error) {
+	var builder strings.Builder
+	first := true
+	for _, tp := range types {
+		if first {
+			first = false
+		} else {
+			builder.WriteString("\n\n")
+		}
+		if err := writeType(&builder, tp, types); err != nil {
+			return "", apiutil.WrapErr(err, "Type "+tp.Name+" generate error")
+		}
+	}
+
+	return builder.String(), nil
+}
+
+func genTypes(dir string, api *spec.ApiSpec) error {
+	val, err := BuildTypes(api.Types)
+	if err != nil {
+		return err
+	}
+
+	filename := path.Join(dir, typesDir, typesFile)
+	if err := util.RemoveOrQuit(filename); err != nil {
+		return err
+	}
+
+	fp, created, err := apiutil.MaybeCreateFile(dir, typesDir, typesFile)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	t := template.Must(template.New("typesTemplate").Parse(typesTemplate))
+	buffer := new(bytes.Buffer)
+	err = t.Execute(buffer, map[string]interface{}{
+		"types":        val,
+		"containsTime": api.ContainsTime(),
+	})
+	if err != nil {
+		return nil
+	}
+	formatCode := formatCode(buffer.String())
+	_, err = fp.WriteString(formatCode)
+	return err
+}
+
+func convertTypeCase(types []spec.Type, t string) (string, error) {
+	ts, err := apiutil.DecomposeType(t)
+	if err != nil {
+		return "", err
+	}
+
+	var defTypes []string
+	for _, tp := range ts {
+		for _, typ := range types {
+			if typ.Name == tp {
+				defTypes = append(defTypes, tp)
+			}
+
+			if len(typ.Annotations) > 0 {
+				if value, ok := apiutil.GetAnnotationValue(typ.Annotations, "serverReplacer", tp); ok {
+					t = strings.ReplaceAll(t, tp, value)
+				}
+			}
+		}
+	}
+
+	for _, tp := range defTypes {
+		t = strings.ReplaceAll(t, tp, util.Title(tp))
+	}
+
+	return t, nil
+}
+
+func writeType(writer io.Writer, tp spec.Type, types []spec.Type) error {
+	fmt.Fprintf(writer, "type %s struct {\n", util.Title(tp.Name))
+	for _, member := range tp.Members {
+		if member.IsInline {
+			var found = false
+			for _, ty := range types {
+				if strings.ToLower(ty.Name) == strings.ToLower(member.Name) {
+					found = true
+				}
+			}
+			if !found {
+				return errors.New("inline type " + member.Name + " not exist, please correct api file")
+			}
+			if _, err := fmt.Fprintf(writer, "%s\n", strings.Title(member.Type)); err != nil {
+				return err
+			} else {
+				continue
+			}
+		}
+		tpString, err := convertTypeCase(types, member.Type)
+		if err != nil {
+			return err
+		}
+		pm, err := member.GetPropertyName()
+		if err != nil {
+			return err
+		}
+		if !strings.Contains(pm, "_") {
+			if strings.Title(member.Name) != strings.Title(pm) {
+				fmt.Printf("type: %s, property name %s json tag illegal, "+
+					"should set json tag as `json:\"%s\"` \n", tp.Name, member.Name, util.Untitle(member.Name))
+			}
+		}
+		if err := writeProperty(writer, member.Name, tpString, member.Tag, member.GetComment(), 1); err != nil {
+			return err
+		}
+	}
+	fmt.Fprintf(writer, "}")
+	return nil
+}

+ 67 - 0
tools/goctl/api/gogen/util.go

@@ -0,0 +1,67 @@
+package gogen
+
+import (
+	"fmt"
+	goformat "go/format"
+	"io"
+	"path/filepath"
+	"strings"
+
+	"zero/core/collection"
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/api/util"
+	"zero/tools/goctl/vars"
+)
+
+func getParentPackage(dir string) (string, error) {
+	absDir, err := filepath.Abs(dir)
+	if err != nil {
+		return "", err
+	}
+	pos := strings.Index(absDir, vars.ProjectName)
+	if pos < 0 {
+		return "", fmt.Errorf("%s not in project directory", dir)
+	}
+
+	return absDir[pos:], nil
+}
+
+func writeIndent(writer io.Writer, indent int) {
+	for i := 0; i < indent; i++ {
+		fmt.Fprint(writer, "\t")
+	}
+}
+
+func writeProperty(writer io.Writer, name, tp, tag, comment string, indent int) error {
+	writeIndent(writer, indent)
+	var err error
+	if len(comment) > 0 {
+		comment = strings.TrimPrefix(comment, "//")
+		comment = "//" + comment
+		_, err = fmt.Fprintf(writer, "%s %s %s %s\n", strings.Title(name), tp, tag, comment)
+	} else {
+		_, err = fmt.Fprintf(writer, "%s %s %s\n", strings.Title(name), tp, tag)
+	}
+	return err
+}
+
+func getAuths(api *spec.ApiSpec) []string {
+	var authNames = collection.NewSet()
+	for _, g := range api.Service.Groups {
+		if value, ok := util.GetAnnotationValue(g.Annotations, "server", "jwt"); ok {
+			authNames.Add(value)
+		}
+		if value, ok := util.GetAnnotationValue(g.Annotations, "server", "signature"); ok {
+			authNames.Add(value)
+		}
+	}
+	return authNames.KeysStr()
+}
+
+func formatCode(code string) string {
+	ret, err := goformat.Source([]byte(code))
+	if err != nil {
+		return code
+	}
+	return string(ret)
+}

+ 12 - 0
tools/goctl/api/gogen/vars.go

@@ -0,0 +1,12 @@
+package gogen
+
+const (
+	interval       = "internal/"
+	typesPacket    = "types"
+	configDir      = interval + "config"
+	contextDir     = interval + "svc"
+	handlerDir     = interval + "handler"
+	logicDir       = interval + "logic"
+	typesDir       = interval + typesPacket
+	folderProperty = "folder"
+)

+ 46 - 0
tools/goctl/api/javagen/gen.go

@@ -0,0 +1,46 @@
+package javagen
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"zero/core/lang"
+	"zero/tools/goctl/api/parser"
+	"zero/tools/goctl/util"
+
+	"github.com/logrusorgru/aurora"
+	"github.com/urfave/cli"
+)
+
+func JavaCommand(c *cli.Context) error {
+	apiFile := c.String("api")
+	dir := c.String("dir")
+	if len(apiFile) == 0 {
+		return errors.New("missing -api")
+	}
+	if len(dir) == 0 {
+		return errors.New("missing -dir")
+	}
+
+	p, err := parser.NewParser(apiFile)
+	if err != nil {
+		return err
+	}
+	api, err := p.Parse()
+	if err != nil {
+		return err
+	}
+
+	packetName := api.Service.Name
+	if strings.HasSuffix(packetName, "-api") {
+		packetName = packetName[:len(packetName)-4]
+	}
+
+	lang.Must(util.MkdirIfNotExist(dir))
+	lang.Must(genPacket(dir, packetName, api))
+	lang.Must(genComponents(dir, packetName, api))
+
+	fmt.Println(aurora.Green("Done."))
+	return nil
+}

+ 85 - 0
tools/goctl/api/javagen/gencomponents.go

@@ -0,0 +1,85 @@
+package javagen
+
+import (
+	"fmt"
+	"io"
+	"path"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	apiutil "zero/tools/goctl/api/util"
+	"zero/tools/goctl/util"
+)
+
+const (
+	componentTemplate = `// DO NOT EDIT, generated by goctl
+package com.xhb.logic.http.packet.{{.packet}}.model;
+
+import com.xhb.logic.http.DeProguardable;
+
+{{.componentType}}
+`
+)
+
+func genComponents(dir, packetName string, api *spec.ApiSpec) error {
+	types := apiutil.GetSharedTypes(api)
+	if len(types) == 0 {
+		return nil
+	}
+	for _, ty := range types {
+		if err := createComponent(dir, packetName, ty); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func createComponent(dir, packetName string, ty spec.Type) error {
+	modelFile := util.Title(ty.Name) + ".java"
+	filename := path.Join(dir, modelDir, modelFile)
+	if err := util.RemoveOrQuit(filename); err != nil {
+		return err
+	}
+
+	fp, created, err := apiutil.MaybeCreateFile(dir, modelDir, modelFile)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	tys, err := buildType(ty)
+	if err != nil {
+		return err
+	}
+
+	t := template.Must(template.New("componentType").Parse(componentTemplate))
+	return t.Execute(fp, map[string]string{
+		"componentType": tys,
+		"packet":        packetName,
+	})
+}
+
+func buildType(ty spec.Type) (string, error) {
+	var builder strings.Builder
+	if err := writeType(&builder, ty); err != nil {
+		return "", apiutil.WrapErr(err, "Type "+ty.Name+" generate error")
+	}
+	return builder.String(), nil
+}
+
+func writeType(writer io.Writer, tp spec.Type) error {
+	fmt.Fprintf(writer, "public class %s implements DeProguardable {\n", util.Title(tp.Name))
+	for _, member := range tp.Members {
+		if err := writeProperty(writer, member, 1); err != nil {
+			return err
+		}
+	}
+	genGetSet(writer, tp, 1)
+	fmt.Fprintf(writer, "}\n")
+	return nil
+}

+ 277 - 0
tools/goctl/api/javagen/genpacket.go

@@ -0,0 +1,277 @@
+package javagen
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+	"text/template"
+
+	"zero/core/stringx"
+	"zero/tools/goctl/api/spec"
+	apiutil "zero/tools/goctl/api/util"
+	"zero/tools/goctl/util"
+)
+
+const packetTemplate = `package com.xhb.logic.http.packet.{{.packet}};
+
+import com.google.gson.Gson;
+import com.xhb.commons.JSON;
+import com.xhb.commons.JsonParser;
+import com.xhb.core.network.HttpRequestClient;
+import com.xhb.core.packet.HttpRequestPacket;
+import com.xhb.core.response.HttpResponseData;
+import com.xhb.logic.http.DeProguardable;
+{{.import}}
+
+import org.jetbrains.annotations.NotNull;
+import org.json.JSONObject;
+
+public class {{.packetName}} extends HttpRequestPacket<{{.packetName}}.{{.packetName}}Response> {
+
+	{{.paramsDeclaration}}
+
+	public {{.packetName}}({{.params}}{{.requestType}} request) {
+        super(request);
+		this.request = request;{{.paramsSet}}
+    }
+
+	@Override
+    public HttpRequestClient.Method requestMethod() {
+        return HttpRequestClient.Method.{{.method}};
+    }
+
+	@Override
+    public String requestUri() {
+        return {{.uri}};
+    }
+
+	@Override
+    public {{.packetName}}Response newInstanceFrom(JSON json) {
+        return new {{.packetName}}Response(json);
+    }
+
+	public static class {{.packetName}}Response extends HttpResponseData {
+
+		private {{.responseType}} responseData;
+
+        {{.packetName}}Response(@NotNull JSON json) {
+            super(json);
+            JSONObject jsonObject = json.asObject();
+			if (JsonParser.hasKey(jsonObject, "data")) {
+				Gson gson = new Gson();
+				JSONObject dataJson = JsonParser.getJSONObject(jsonObject, "data");
+				responseData = gson.fromJson(dataJson.toString(), {{.responseType}}.class);
+			}
+        }
+
+		public {{.responseType}} get{{.responseType}} () {
+            return responseData;
+        }
+    }
+
+	{{.types}}
+}
+`
+
+func genPacket(dir, packetName string, api *spec.ApiSpec) error {
+	for _, route := range api.Service.Routes {
+		if err := createWith(dir, api, route, packetName); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func createWith(dir string, api *spec.ApiSpec, route spec.Route, packetName string) error {
+	packet, ok := apiutil.GetAnnotationValue(route.Annotations, "server", "handler")
+	packet = strings.Replace(packet, "Handler", "Packet", 1)
+	if !ok {
+		return fmt.Errorf("missing packet annotation for %q", route.Path)
+	}
+
+	javaFile := packet + ".java"
+	fp, created, err := apiutil.MaybeCreateFile(dir, "", javaFile)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	var builder strings.Builder
+	var first bool
+	tps := apiutil.GetLocalTypes(api, route)
+
+	for _, tp := range tps {
+		if first {
+			first = false
+		} else {
+			fmt.Fprintln(&builder)
+		}
+		if err := genType(&builder, tp); err != nil {
+			return err
+		}
+	}
+	types := builder.String()
+	writeIndent(&builder, 1)
+
+	params := paramsForRoute(route)
+	paramsDeclaration := declarationForRoute(route)
+	paramsSet := paramsSet(route)
+
+	t := template.Must(template.New("packetTemplate").Parse(packetTemplate))
+	var tmplBytes bytes.Buffer
+	err = t.Execute(&tmplBytes, map[string]string{
+		"packetName":        packet,
+		"method":            strings.ToUpper(route.Method),
+		"uri":               processUri(route),
+		"types":             strings.TrimSpace(types),
+		"responseType":      stringx.TakeOne(util.Title(route.ResponseType.Name), "Object"),
+		"params":            params,
+		"paramsDeclaration": strings.TrimSpace(paramsDeclaration),
+		"paramsSet":         paramsSet,
+		"packet":            packetName,
+		"requestType":       util.Title(route.RequestType.Name),
+		"import":            getImports(api, route, packetName),
+	})
+	if err != nil {
+		return err
+	}
+	formatFile(&tmplBytes, fp)
+	return nil
+}
+
+func getImports(api *spec.ApiSpec, route spec.Route, packetName string) string {
+	var builder strings.Builder
+	allTypes := apiutil.GetAllTypes(api, route)
+	sharedTypes := apiutil.GetSharedTypes(api)
+	for _, at := range allTypes {
+		for _, item := range sharedTypes {
+			if item.Name == at.Name {
+				fmt.Fprintf(&builder, "import com.xhb.logic.http.packet.%s.model.%s;\n", packetName, item.Name)
+				break
+			}
+		}
+	}
+	return builder.String()
+}
+
+func formatFile(tmplBytes *bytes.Buffer, file *os.File) {
+	scanner := bufio.NewScanner(tmplBytes)
+	builder := bufio.NewWriter(file)
+	defer builder.Flush()
+	preIsBreakLine := false
+	for scanner.Scan() {
+		text := strings.TrimSpace(scanner.Text())
+		if text == "" && preIsBreakLine {
+			continue
+		}
+		preIsBreakLine = text == ""
+		builder.WriteString(scanner.Text() + "\n")
+	}
+	if err := scanner.Err(); err != nil {
+		println(err)
+	}
+}
+
+func paramsSet(route spec.Route) string {
+	path := route.Path
+	cops := strings.Split(path, "/")
+	var builder strings.Builder
+	for _, cop := range cops {
+		if len(cop) == 0 {
+			continue
+		}
+		if strings.HasPrefix(cop, ":") {
+			param := cop[1:]
+			builder.WriteString("\n")
+			builder.WriteString(fmt.Sprintf("\t\tthis.%s = %s;", param, param))
+		}
+	}
+	result := builder.String()
+	return result
+}
+
+func paramsForRoute(route spec.Route) string {
+	path := route.Path
+	cops := strings.Split(path, "/")
+	var builder strings.Builder
+	for _, cop := range cops {
+		if len(cop) == 0 {
+			continue
+		}
+		if strings.HasPrefix(cop, ":") {
+			builder.WriteString(fmt.Sprintf("String %s, ", cop[1:]))
+		}
+	}
+	return builder.String()
+}
+
+func declarationForRoute(route spec.Route) string {
+	path := route.Path
+	cops := strings.Split(path, "/")
+	var builder strings.Builder
+	writeIndent(&builder, 1)
+	for _, cop := range cops {
+		if len(cop) == 0 {
+			continue
+		}
+		if strings.HasPrefix(cop, ":") {
+			writeIndent(&builder, 1)
+			builder.WriteString(fmt.Sprintf("private String %s;\n", cop[1:]))
+		}
+	}
+	result := strings.TrimSpace(builder.String())
+	if len(result) > 0 {
+		result = "\n" + result
+	}
+	return result
+}
+
+func processUri(route spec.Route) string {
+	path := route.Path
+	var builder strings.Builder
+	cops := strings.Split(path, "/")
+	for index, cop := range cops {
+		if len(cop) == 0 {
+			continue
+		}
+		if strings.HasPrefix(cop, ":") {
+			builder.WriteString("/\" + " + cop[1:] + " + \"")
+		} else {
+			builder.WriteString("/" + cop)
+			if index == len(cops)-1 {
+				builder.WriteString("\"")
+			}
+		}
+	}
+	result := builder.String()
+	if strings.HasSuffix(result, " + \"") {
+		result = result[:len(result)-4]
+	}
+	if strings.HasPrefix(result, "/") {
+		result = "\"" + result
+	}
+	return result
+}
+
+func genType(writer io.Writer, tp spec.Type) error {
+	writeIndent(writer, 1)
+	fmt.Fprintf(writer, "static class %s implements DeProguardable {\n", util.Title(tp.Name))
+	for _, member := range tp.Members {
+		if err := writeProperty(writer, member, 2); err != nil {
+			return err
+		}
+	}
+	writeBreakline(writer)
+	writeIndent(writer, 1)
+	genGetSet(writer, tp, 2)
+	writeIndent(writer, 1)
+	fmt.Fprintln(writer, "}")
+	return nil
+}

+ 163 - 0
tools/goctl/api/javagen/util.go

@@ -0,0 +1,163 @@
+package javagen
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	apiutil "zero/tools/goctl/api/util"
+	"zero/tools/goctl/util"
+)
+
+const getSetTemplate = `
+{{.indent}}{{.decorator}}
+{{.indent}}public {{.returnType}} get{{.property}}() {
+{{.indent}}	return this.{{.propertyValue}};
+{{.indent}}}
+
+{{.indent}}public void set{{.property}}({{.type}} {{.propertyValue}}) {
+{{.indent}}	this.{{.propertyValue}} = {{.propertyValue}};
+{{.indent}}}
+`
+
+func writeProperty(writer io.Writer, member spec.Member, indent int) error {
+	writeIndent(writer, indent)
+	ty, err := goTypeToJava(member.Type)
+	ty = strings.Replace(ty, "*", "", 1)
+	if err != nil {
+		return err
+	}
+	name, err := member.GetPropertyName()
+	if err != nil {
+		return err
+	}
+	_, err = fmt.Fprintf(writer, "private %s %s", ty, name)
+	if err != nil {
+		return err
+	}
+	writeDefaultValue(writer, member)
+	fmt.Fprint(writer, ";\n")
+	return err
+}
+
+func writeDefaultValue(writer io.Writer, member spec.Member) error {
+	switch member.Type {
+	case "string":
+		_, err := fmt.Fprintf(writer, " = \"\"")
+		return err
+	}
+	return nil
+}
+
+func writeIndent(writer io.Writer, indent int) {
+	for i := 0; i < indent; i++ {
+		fmt.Fprint(writer, "\t")
+	}
+}
+
+func indentString(indent int) string {
+	var result = ""
+	for i := 0; i < indent; i++ {
+		result += "\t"
+	}
+	return result
+}
+
+func writeBreakline(writer io.Writer) {
+	fmt.Fprint(writer, "\n")
+}
+
+func isPrimitiveType(tp string) bool {
+	switch tp {
+	case "int", "int32", "int64":
+		return true
+	case "float", "float32", "float64":
+		return true
+	case "bool":
+		return true
+	}
+	return false
+}
+
+func goTypeToJava(tp string) (string, error) {
+	if len(tp) == 0 {
+		return "", errors.New("property type empty")
+	}
+	if strings.HasPrefix(tp, "*") {
+		tp = tp[1:]
+	}
+	switch tp {
+	case "string":
+		return "String", nil
+	case "int64":
+		return "long", nil
+	case "int", "int8", "int32":
+		return "int", nil
+	case "float", "float32", "float64":
+		return "double", nil
+	case "bool":
+		return "boolean", nil
+	}
+	if strings.HasPrefix(tp, "[]") {
+		tys, err := apiutil.DecomposeType(tp)
+		if err != nil {
+			return "", err
+		}
+		if len(tys) == 0 {
+			return "", fmt.Errorf("%s tp parse error", tp)
+		}
+		return fmt.Sprintf("java.util.ArrayList<%s>", util.Title(tys[0])), nil
+	} else if strings.HasPrefix(tp, "map") {
+		tys, err := apiutil.DecomposeType(tp)
+		if err != nil {
+			return "", err
+		}
+		if len(tys) == 2 {
+			return "", fmt.Errorf("%s tp parse error", tp)
+		}
+		return fmt.Sprintf("java.util.HashMap<String, %s>", util.Title(tys[1])), nil
+	}
+	return util.Title(tp), nil
+}
+
+func genGetSet(writer io.Writer, tp spec.Type, indent int) error {
+	t := template.Must(template.New("getSetTemplate").Parse(getSetTemplate))
+	for _, member := range tp.Members {
+		var tmplBytes bytes.Buffer
+
+		oty, err := goTypeToJava(member.Type)
+		if err != nil {
+			return err
+		}
+		tyString := oty
+		decorator := ""
+		if !isPrimitiveType(member.Type) {
+			if member.IsOptional() {
+				decorator = "@org.jetbrains.annotations.Nullable "
+			} else {
+				decorator = "@org.jetbrains.annotations.NotNull "
+			}
+			tyString = decorator + tyString
+		}
+
+		err = t.Execute(&tmplBytes, map[string]string{
+			"property":      util.Title(member.Name),
+			"propertyValue": util.Untitle(member.Name),
+			"type":          tyString,
+			"decorator":     decorator,
+			"returnType":    oty,
+			"indent":        indentString(indent),
+		})
+		if err != nil {
+			return err
+		}
+		r := tmplBytes.String()
+		r = strings.Replace(r, " boolean get", " boolean is", 1)
+		writer.Write([]byte(r))
+	}
+	return nil
+}

+ 3 - 0
tools/goctl/api/javagen/vars.go

@@ -0,0 +1,3 @@
+package javagen
+
+const modelDir = "model"

+ 21 - 0
tools/goctl/api/main.go

@@ -0,0 +1,21 @@
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"zero/core/lang"
+	"zero/tools/goctl/api/parser"
+)
+
+func main() {
+	if len(os.Args) <= 1 {
+		return
+	}
+
+	p, err := parser.NewParser(os.Args[1])
+	lang.Must(err)
+	api, err := p.Parse()
+	lang.Must(err)
+	fmt.Println(api)
+}

+ 182 - 0
tools/goctl/api/parser/basestate.go

@@ -0,0 +1,182 @@
+package parser
+
+import (
+	"bufio"
+	"fmt"
+	"strings"
+)
+
+const (
+	startState = iota
+	attrNameState
+	attrValueState
+	attrColonState
+	multilineState
+)
+
+type baseState struct {
+	r          *bufio.Reader
+	lineNumber *int
+}
+
+func newBaseState(r *bufio.Reader, lineNumber *int) *baseState {
+	return &baseState{
+		r:          r,
+		lineNumber: lineNumber,
+	}
+}
+
+func (s *baseState) parseProperties() (map[string]string, error) {
+	var r = s.r
+	var attributes = make(map[string]string)
+	var builder strings.Builder
+	var key string
+	var st = startState
+
+	for {
+		ch, err := s.read()
+		if err != nil {
+			return nil, err
+		}
+
+		switch st {
+		case startState:
+			switch {
+			case isNewline(ch):
+				return nil, fmt.Errorf("%q should be on the same line with %q", leftParenthesis, infoDirective)
+			case isSpace(ch):
+				continue
+			case ch == leftParenthesis:
+				st = attrNameState
+			default:
+				return nil, fmt.Errorf("unexpected char %q after %q", ch, infoDirective)
+			}
+		case attrNameState:
+			switch {
+			case isNewline(ch):
+				if builder.Len() > 0 {
+					return nil, fmt.Errorf("unexpected newline after %q", builder.String())
+				}
+			case isLetterDigit(ch):
+				builder.WriteRune(ch)
+			case isSpace(ch):
+				if builder.Len() > 0 {
+					key = builder.String()
+					builder.Reset()
+					st = attrColonState
+				}
+			case ch == colon:
+				if builder.Len() == 0 {
+					return nil, fmt.Errorf("unexpected leading %q", ch)
+				}
+				key = builder.String()
+				builder.Reset()
+				st = attrValueState
+			case ch == rightParenthesis:
+				return attributes, nil
+			}
+		case attrColonState:
+			switch {
+			case isSpace(ch):
+				continue
+			case ch == colon:
+				st = attrValueState
+			default:
+				return nil, fmt.Errorf("bad char %q after %q in %q", ch, key, infoDirective)
+			}
+		case attrValueState:
+			switch {
+			case ch == multilineBeginTag:
+				if builder.Len() > 0 {
+					return nil, fmt.Errorf("%q before %q", builder.String(), multilineBeginTag)
+				} else {
+					st = multilineState
+				}
+			case isSpace(ch):
+				if builder.Len() > 0 {
+					builder.WriteRune(ch)
+				}
+			case isNewline(ch):
+				attributes[key] = builder.String()
+				builder.Reset()
+				st = attrNameState
+			case ch == rightParenthesis:
+				attributes[key] = builder.String()
+				builder.Reset()
+				return attributes, nil
+			default:
+				builder.WriteRune(ch)
+			}
+		case multilineState:
+			switch {
+			case ch == multilineEndTag:
+				attributes[key] = builder.String()
+				builder.Reset()
+				st = attrNameState
+			case isNewline(ch):
+				var multipleNewlines bool
+			loopAfterNewline:
+				for {
+					next, err := read(r)
+					if err != nil {
+						return nil, err
+					}
+
+					switch {
+					case isSpace(next):
+						continue
+					case isNewline(next):
+						multipleNewlines = true
+					default:
+						if err := unread(r); err != nil {
+							return nil, err
+						}
+						break loopAfterNewline
+					}
+				}
+
+				if multipleNewlines {
+					fmt.Fprintln(&builder)
+				} else {
+					builder.WriteByte(' ')
+				}
+			case ch == rightParenthesis:
+				if builder.Len() > 0 {
+					attributes[key] = builder.String()
+					builder.Reset()
+				}
+				return attributes, nil
+			default:
+				builder.WriteRune(ch)
+			}
+		}
+	}
+}
+
+func (s *baseState) read() (rune, error) {
+	value, err := read(s.r)
+	if err != nil {
+		return 0, err
+	}
+	if isNewline(value) {
+		*s.lineNumber++
+	}
+	return value, nil
+}
+
+func (s *baseState) readLine() (string, error) {
+	line, _, err := s.r.ReadLine()
+	if err != nil {
+		return "", err
+	}
+	*s.lineNumber++
+	return string(line), nil
+}
+
+func (s *baseState) skipSpaces() error {
+	return skipSpaces(s.r)
+}
+
+func (s *baseState) unread() error {
+	return unread(s.r)
+}

+ 20 - 0
tools/goctl/api/parser/basestate_test.go

@@ -0,0 +1,20 @@
+package parser
+
+import (
+	"bufio"
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProperties(t *testing.T) {
+	const text = `(summary: hello world)`
+	var builder bytes.Buffer
+	builder.WriteString(text)
+	var lineNumber = 1
+	var state = newBaseState(bufio.NewReader(&builder), &lineNumber)
+	m, err := state.parseProperties()
+	assert.Nil(t, err)
+	assert.Equal(t, "hello world", m["summary"])
+}

+ 132 - 0
tools/goctl/api/parser/entity.go

@@ -0,0 +1,132 @@
+package parser
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+)
+
+type (
+	entity struct {
+		state  *baseState
+		api    *spec.ApiSpec
+		parser entityParser
+	}
+
+	entityParser interface {
+		parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error
+		setEntityName(name string)
+	}
+)
+
+func newEntity(state *baseState, api *spec.ApiSpec, parser entityParser) entity {
+	return entity{
+		state:  state,
+		api:    api,
+		parser: parser,
+	}
+}
+
+func (s *entity) process() error {
+	line, err := s.state.readLine()
+	if err != nil {
+		return err
+	}
+
+	fields := strings.Fields(line)
+	if len(fields) < 2 {
+		return fmt.Errorf("invalid type definition for %q",
+			strings.TrimSpace(strings.Trim(string(line), "{")))
+	}
+
+	if len(fields) == 2 {
+		if fields[1] != leftBrace {
+			return fmt.Errorf("bad string %q after type", fields[1])
+		}
+	} else if len(fields) == 3 {
+		if fields[1] != typeStruct {
+			return fmt.Errorf("bad string %q after type", fields[1])
+		}
+		if fields[2] != leftBrace {
+			return fmt.Errorf("bad string %q after type", fields[2])
+		}
+	}
+
+	s.parser.setEntityName(fields[0])
+
+	var annos []spec.Annotation
+memberLoop:
+	for {
+		ch, err := s.state.read()
+		if err != nil {
+			return err
+		}
+
+		var annoName string
+		var builder strings.Builder
+		switch {
+		case ch == at:
+		annotationLoop:
+			for {
+				next, err := s.state.read()
+				if err != nil {
+					return err
+				}
+				switch {
+				case isSpace(next):
+					if builder.Len() > 0 {
+						annoName = builder.String()
+						builder.Reset()
+					}
+				case isNewline(next):
+					if builder.Len() == 0 {
+						return errors.New("invalid annotation format")
+					}
+				case next == leftParenthesis:
+					if builder.Len() == 0 {
+						return errors.New("invalid annotation format")
+					}
+					annoName = builder.String()
+					builder.Reset()
+					if err := s.state.unread(); err != nil {
+						return err
+					}
+					attrs, err := s.state.parseProperties()
+					if err != nil {
+						return err
+					}
+					annos = append(annos, spec.Annotation{
+						Name:       annoName,
+						Properties: attrs,
+					})
+					break annotationLoop
+				default:
+					builder.WriteRune(next)
+				}
+			}
+		case ch == rightBrace:
+			break memberLoop
+		case isLetterDigit(ch):
+			if err := s.state.unread(); err != nil {
+				return err
+			}
+
+			var line string
+			line, err = s.state.readLine()
+			if err != nil {
+				return err
+			}
+
+			line = strings.TrimSpace(line)
+			if err := s.parser.parseLine(line, s.api, annos); err != nil {
+				return err
+			}
+
+			annos = nil
+		}
+	}
+
+	return nil
+}

+ 62 - 0
tools/goctl/api/parser/infostate.go

@@ -0,0 +1,62 @@
+package parser
+
+import (
+	"fmt"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+)
+
+const (
+	titleTag   = "title"
+	descTag    = "desc"
+	versionTag = "version"
+	authorTag  = "author"
+	emailTag   = "email"
+)
+
+type infoState struct {
+	*baseState
+	innerState int
+}
+
+func newInfoState(st *baseState) state {
+	return &infoState{
+		baseState:  st,
+		innerState: startState,
+	}
+}
+
+func (s *infoState) process(api *spec.ApiSpec) (state, error) {
+	attrs, err := s.parseProperties()
+	if err != nil {
+		return nil, err
+	}
+
+	if err := s.writeInfo(api, attrs); err != nil {
+		return nil, err
+	}
+
+	return newRootState(s.r, s.lineNumber), nil
+}
+
+func (s *infoState) writeInfo(api *spec.ApiSpec, attrs map[string]string) error {
+	for k, v := range attrs {
+		switch k {
+		case titleTag:
+			api.Info.Title = strings.TrimSpace(v)
+		case descTag:
+			api.Info.Desc = strings.TrimSpace(v)
+		case versionTag:
+			api.Info.Version = strings.TrimSpace(v)
+		case authorTag:
+			api.Info.Author = strings.TrimSpace(v)
+		case emailTag:
+			api.Info.Email = strings.TrimSpace(v)
+		default:
+			return fmt.Errorf("unknown directive %q in %q section", k, infoDirective)
+		}
+	}
+
+	return nil
+}

+ 57 - 0
tools/goctl/api/parser/parser.go

@@ -0,0 +1,57 @@
+package parser
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+
+	"zero/tools/goctl/api/spec"
+)
+
+type Parser struct {
+	r  *bufio.Reader
+	st string
+}
+
+func NewParser(filename string) (*Parser, error) {
+	api, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+	info, body, service, err := MatchStruct(string(api))
+	if err != nil {
+		return nil, err
+	}
+	var buffer = new(bytes.Buffer)
+	buffer.WriteString(info)
+	buffer.WriteString(service)
+	return &Parser{
+		r:  bufio.NewReader(buffer),
+		st: body,
+	}, nil
+}
+
+func (p *Parser) Parse() (api *spec.ApiSpec, err error) {
+	api = new(spec.ApiSpec)
+	types, err := parseStructAst(p.st)
+	if err != nil {
+		return nil, err
+	}
+	api.Types = types
+	var lineNumber = 1
+	st := newRootState(p.r, &lineNumber)
+	for {
+		st, err = st.process(api)
+		if err == io.EOF {
+			return api, p.validate(api)
+		}
+		if err != nil {
+			return nil, fmt.Errorf("near line: %d, %s", lineNumber, err.Error())
+		}
+		if st == nil {
+			return api, p.validate(api)
+		}
+	}
+}

+ 109 - 0
tools/goctl/api/parser/rootstate.go

@@ -0,0 +1,109 @@
+package parser
+
+import (
+	"bufio"
+	"fmt"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+)
+
+type rootState struct {
+	*baseState
+}
+
+func newRootState(r *bufio.Reader, lineNumber *int) state {
+	var state = newBaseState(r, lineNumber)
+	return rootState{
+		baseState: state,
+	}
+}
+
+func (s rootState) process(api *spec.ApiSpec) (state, error) {
+	var annos []spec.Annotation
+	var builder strings.Builder
+	for {
+		ch, err := s.read()
+		if err != nil {
+			return nil, err
+		}
+
+		switch {
+		case isSpace(ch):
+			if builder.Len() == 0 {
+				continue
+			}
+			token := builder.String()
+			builder.Reset()
+			return s.processToken(token, annos)
+		case ch == at:
+			if builder.Len() > 0 {
+				return nil, fmt.Errorf("%q before %q", builder.String(), at)
+			}
+
+			var annoName string
+		annoLoop:
+			for {
+				next, err := s.read()
+				if err != nil {
+					return nil, err
+				}
+				switch {
+				case isSpace(next):
+					if builder.Len() > 0 {
+						annoName = builder.String()
+						builder.Reset()
+					}
+				case next == leftParenthesis:
+					if err := s.unread(); err != nil {
+						return nil, err
+					}
+					if builder.Len() > 0 {
+						annoName = builder.String()
+						builder.Reset()
+					}
+					attrs, err := s.parseProperties()
+					if err != nil {
+						return nil, err
+					}
+					annos = append(annos, spec.Annotation{
+						Name:       annoName,
+						Properties: attrs,
+					})
+					break annoLoop
+				default:
+					builder.WriteRune(next)
+				}
+			}
+		case ch == leftParenthesis:
+			if builder.Len() == 0 {
+				return nil, fmt.Errorf("incorrect %q at the beginning of the line", leftParenthesis)
+			}
+			if err := s.unread(); err != nil {
+				return nil, err
+			}
+			token := builder.String()
+			builder.Reset()
+			return s.processToken(token, annos)
+		case isLetterDigit(ch):
+			builder.WriteRune(ch)
+		case isNewline(ch):
+			if builder.Len() > 0 {
+				return nil, fmt.Errorf("incorrect newline after %q", builder.String())
+			}
+		}
+	}
+}
+
+func (s rootState) processToken(token string, annos []spec.Annotation) (state, error) {
+	switch token {
+	case infoDirective:
+		return newInfoState(s.baseState), nil
+	//case typeDirective:
+	//return newTypeState(s.baseState, annos), nil
+	case serviceDirective:
+		return newServiceState(s.baseState, annos), nil
+	default:
+		return nil, fmt.Errorf("wrong directive %q", token)
+	}
+}

+ 97 - 0
tools/goctl/api/parser/servicestate.go

@@ -0,0 +1,97 @@
+package parser
+
+import (
+	"fmt"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+)
+
+type serviceState struct {
+	*baseState
+	annos []spec.Annotation
+}
+
+func newServiceState(state *baseState, annos []spec.Annotation) state {
+	return &serviceState{
+		baseState: state,
+		annos:     annos,
+	}
+}
+
+func (s *serviceState) process(api *spec.ApiSpec) (state, error) {
+	var name string
+	var routes []spec.Route
+	parser := &serviceEntityParser{
+		acceptName: func(n string) {
+			name = n
+		},
+		acceptRoute: func(route spec.Route) {
+			routes = append(routes, route)
+		},
+	}
+	ent := newEntity(s.baseState, api, parser)
+	if err := ent.process(); err != nil {
+		return nil, err
+	}
+
+	api.Service = spec.Service{
+		Name:        name,
+		Annotations: append(api.Service.Annotations, s.annos...),
+		Routes:      append(api.Service.Routes, routes...),
+		Groups: append(api.Service.Groups, spec.Group{
+			Annotations: s.annos,
+			Routes:      routes,
+		}),
+	}
+
+	return newRootState(s.r, s.lineNumber), nil
+}
+
+type serviceEntityParser struct {
+	acceptName  func(name string)
+	acceptRoute func(route spec.Route)
+}
+
+func (p *serviceEntityParser) parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error {
+	fields := strings.Fields(line)
+	if len(fields) < 2 {
+		return fmt.Errorf("wrong line %q", line)
+	}
+
+	method := fields[0]
+	pathAndRequest := fields[1]
+	pos := strings.Index(pathAndRequest, "(")
+	if pos < 0 {
+		return fmt.Errorf("wrong line %q", line)
+	}
+	path := strings.TrimSpace(pathAndRequest[:pos])
+	pathAndRequest = pathAndRequest[pos+1:]
+	pos = strings.Index(pathAndRequest, ")")
+	if pos < 0 {
+		return fmt.Errorf("wrong line %q", line)
+	}
+	req := pathAndRequest[:pos]
+	var returns string
+	if len(fields) > 2 {
+		returns = fields[2]
+	}
+	returns = strings.ReplaceAll(returns, "returns", "")
+	returns = strings.ReplaceAll(returns, "(", "")
+	returns = strings.ReplaceAll(returns, ")", "")
+	returns = strings.TrimSpace(returns)
+
+	p.acceptRoute(spec.Route{
+		Annotations:  annos,
+		Method:       method,
+		Path:         path,
+		RequestType:  GetType(api, req),
+		ResponseType: GetType(api, returns),
+	})
+
+	return nil
+}
+
+func (p *serviceEntityParser) setEntityName(name string) {
+	p.acceptName(name)
+}

+ 7 - 0
tools/goctl/api/parser/state.go

@@ -0,0 +1,7 @@
+package parser
+
+import "zero/tools/goctl/api/spec"
+
+type state interface {
+	process(api *spec.ApiSpec) (state, error)
+}

+ 329 - 0
tools/goctl/api/parser/typeparser.go

@@ -0,0 +1,329 @@
+package parser
+
+import (
+	"errors"
+	"fmt"
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"sort"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+)
+
+var (
+	ErrStructNotFound      = errors.New("struct not found")
+	ErrUnSupportType       = errors.New("unsupport type")
+	ErrUnSupportInlineType = errors.New("unsupport inline type")
+	interfaceExpr          = `interface{}`
+	objectM                = make(map[string]*spec.Type)
+)
+
+const (
+	golangF = `package ast
+	%s
+`
+	pkgPrefix = "package"
+)
+
+func parseStructAst(golang string) ([]spec.Type, error) {
+	if !strings.HasPrefix(golang, pkgPrefix) {
+		golang = fmt.Sprintf(golangF, golang)
+	}
+	fSet := token.NewFileSet()
+	f, err := parser.ParseFile(fSet, "", golang, parser.ParseComments)
+	if err != nil {
+		return nil, err
+	}
+	commentMap := ast.NewCommentMap(fSet, f, f.Comments)
+	f.Comments = commentMap.Filter(f).Comments()
+	scope := f.Scope
+	if scope == nil {
+		return nil, ErrStructNotFound
+	}
+	objects := scope.Objects
+	structs := make([]*spec.Type, 0)
+	for structName, obj := range objects {
+		st, err := parseObject(structName, obj)
+		if err != nil {
+			return nil, err
+		}
+		structs = append(structs, st)
+	}
+	sort.Slice(structs, func(i, j int) bool {
+		return structs[i].Name < structs[j].Name
+	})
+	resp := make([]spec.Type, 0)
+	for _, item := range structs {
+		resp = append(resp, *item)
+	}
+	return resp, nil
+}
+
+func parseObject(structName string, obj *ast.Object) (*spec.Type, error) {
+	if data, ok := objectM[structName]; ok {
+		return data, nil
+	}
+	var st spec.Type
+	st.Name = structName
+	if obj.Decl == nil {
+		objectM[structName] = &st
+		return &st, nil
+	}
+	decl, ok := obj.Decl.(*ast.TypeSpec)
+	if !ok {
+		objectM[structName] = &st
+		return &st, nil
+	}
+	if decl.Type == nil {
+		objectM[structName] = &st
+		return &st, nil
+	}
+	tp, ok := decl.Type.(*ast.StructType)
+	if !ok {
+		objectM[structName] = &st
+		return &st, nil
+	}
+	fields := tp.Fields
+	if fields == nil {
+		objectM[structName] = &st
+		return &st, nil
+	}
+	fieldList := fields.List
+	members, err := parseFields(fieldList)
+	if err != nil {
+		return nil, err
+	}
+	st.Members = members
+	objectM[structName] = &st
+	return &st, nil
+}
+
+func parseFields(fields []*ast.Field) ([]spec.Member, error) {
+	members := make([]spec.Member, 0)
+	for _, field := range fields {
+		docs := parseCommentOrDoc(field.Doc)
+		comments := parseCommentOrDoc(field.Comment)
+		name := parseName(field.Names)
+		tp, stringExpr, err := parseType(field.Type)
+		if err != nil {
+			return nil, err
+		}
+		tag := parseTag(field.Tag)
+		isInline := name == ""
+		if isInline {
+			var err error
+			name, err = getInlineName(tp)
+			if err != nil {
+				return nil, err
+			}
+		}
+		members = append(members, spec.Member{
+			Name:     name,
+			Type:     stringExpr,
+			Expr:     tp,
+			Tag:      tag,
+			Comments: comments,
+			Docs:     docs,
+			IsInline: isInline,
+		})
+
+	}
+	return members, nil
+}
+
+func getInlineName(tp interface{}) (string, error) {
+	switch v := tp.(type) {
+	case *spec.Type:
+		return v.Name, nil
+	case *spec.PointerType:
+		return getInlineName(v.Star)
+	case *spec.StructType:
+		return v.StringExpr, nil
+	default:
+		return "", ErrUnSupportInlineType
+	}
+}
+
+func getInlineTypePrefix(tp interface{}) (string, error) {
+	if tp == nil {
+		return "", nil
+	}
+	switch tp.(type) {
+	case *ast.Ident:
+		return "", nil
+	case *ast.StarExpr:
+		return "*", nil
+	case *ast.TypeSpec:
+		return "", nil
+	default:
+		return "", ErrUnSupportInlineType
+	}
+}
+
+func parseTag(basicLit *ast.BasicLit) string {
+	if basicLit == nil {
+		return ""
+	}
+	return basicLit.Value
+}
+
+// returns
+// resp1:type can convert to *spec.PointerType|*spec.BasicType|*spec.MapType|*spec.ArrayType|*spec.InterfaceType
+// resp2:type's string expression,like int、string、[]int64、map[string]User、*User
+// resp3:error
+func parseType(expr ast.Expr) (interface{}, string, error) {
+	if expr == nil {
+		return nil, "", ErrUnSupportType
+	}
+	switch v := expr.(type) {
+	case *ast.StarExpr:
+		star, stringExpr, err := parseType(v.X)
+		if err != nil {
+			return nil, "", err
+		}
+		e := fmt.Sprintf("*%s", stringExpr)
+		return &spec.PointerType{Star: star, StringExpr: e}, e, nil
+	case *ast.Ident:
+		if isBasicType(v.Name) {
+			return &spec.BasicType{Name: v.Name, StringExpr: v.Name}, v.Name, nil
+		} else if v.Obj != nil {
+			obj := v.Obj
+			if obj.Name != v.Name { // 防止引用自己而无限递归
+				specType, err := parseObject(v.Name, v.Obj)
+				if err != nil {
+					return nil, "", err
+				} else {
+					return specType, v.Obj.Name, nil
+				}
+			} else {
+				inlineType, err := getInlineTypePrefix(obj.Decl)
+				if err != nil {
+					return nil, "", err
+				}
+				return &spec.StructType{
+					StringExpr: fmt.Sprintf("%s%s", inlineType, v.Name),
+				}, v.Name, nil
+			}
+		} else {
+			return nil, "", fmt.Errorf(" [%s] - member is not exist", v.Name)
+		}
+	case *ast.MapType:
+		key, keyStringExpr, err := parseType(v.Key)
+		if err != nil {
+			return nil, "", err
+		}
+		value, valueStringExpr, err := parseType(v.Value)
+		if err != nil {
+			return nil, "", err
+		}
+		keyType, ok := key.(*spec.BasicType)
+		if !ok {
+			return nil, "", fmt.Errorf("[%+v] - unsupport type of map key", v.Key)
+		}
+		e := fmt.Sprintf("map[%s]%s", keyStringExpr, valueStringExpr)
+		return &spec.MapType{
+			Key:        keyType.Name,
+			Value:      value,
+			StringExpr: e,
+		}, e, nil
+	case *ast.ArrayType:
+		arrayType, stringExpr, err := parseType(v.Elt)
+		if err != nil {
+			return nil, "", err
+		}
+		e := fmt.Sprintf("[]%s", stringExpr)
+		return &spec.ArrayType{ArrayType: arrayType, StringExpr: e}, e, nil
+	case *ast.InterfaceType:
+		return &spec.InterfaceType{StringExpr: interfaceExpr}, interfaceExpr, nil
+	case *ast.ChanType:
+		return nil, "", errors.New("[chan] - unsupport type")
+	case *ast.FuncType:
+		return nil, "", errors.New("[func] - unsupport type")
+	case *ast.StructType: // todo can optimize
+		return nil, "", errors.New("[struct] - unsupport inline struct type")
+	case *ast.SelectorExpr:
+		x := v.X
+		sel := v.Sel
+		xIdent, ok := x.(*ast.Ident)
+		if ok {
+			name := xIdent.Name
+			if name != "time" && sel.Name != "Time" {
+				return nil, "", fmt.Errorf("[outter package] - package:%s, unsupport type", name)
+			}
+			tm := fmt.Sprintf("time.Time")
+			return &spec.TimeType{
+				StringExpr: tm,
+			}, tm, nil
+		}
+		return nil, "", ErrUnSupportType
+	default:
+		return nil, "", ErrUnSupportType
+	}
+}
+
+func isBasicType(tp string) bool {
+	switch tp {
+	case
+		"bool",
+		"uint8",
+		"uint16",
+		"uint32",
+		"uint64",
+		"int8",
+		"int16",
+		"int32",
+		"int64",
+		"float32",
+		"float64",
+		"complex64",
+		"complex128",
+		"string",
+		"int",
+		"uint",
+		"uintptr",
+		"byte",
+		"rune",
+		"Type",
+		"Type1",
+		"IntegerType",
+		"FloatType",
+		"ComplexType":
+		return true
+	default:
+		return false
+	}
+}
+func parseName(names []*ast.Ident) string {
+	if len(names) == 0 {
+		return ""
+	}
+	name := names[0]
+	return parseIdent(name)
+}
+
+func parseIdent(ident *ast.Ident) string {
+	if ident == nil {
+		return ""
+	}
+	return ident.Name
+}
+
+func parseCommentOrDoc(cg *ast.CommentGroup) []string {
+	if cg == nil {
+		return nil
+	}
+	comments := make([]string, 0)
+	for _, comment := range cg.List {
+		if comment == nil {
+			continue
+		}
+		text := strings.TrimSpace(comment.Text)
+		if text == "" {
+			continue
+		}
+		comments = append(comments, text)
+	}
+	return comments
+}

+ 95 - 0
tools/goctl/api/parser/typestate.go

@@ -0,0 +1,95 @@
+package parser
+
+import (
+	"fmt"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/util"
+)
+
+type typeState struct {
+	*baseState
+	annos []spec.Annotation
+}
+
+func newTypeState(state *baseState, annos []spec.Annotation) state {
+	return &typeState{
+		baseState: state,
+		annos:     annos,
+	}
+}
+
+func (s *typeState) process(api *spec.ApiSpec) (state, error) {
+	var name string
+	var members []spec.Member
+	parser := &typeEntityParser{
+		acceptName: func(n string) {
+			name = n
+		},
+		acceptMember: func(member spec.Member) {
+			members = append(members, member)
+		},
+	}
+	ent := newEntity(s.baseState, api, parser)
+	if err := ent.process(); err != nil {
+		return nil, err
+	}
+
+	api.Types = append(api.Types, spec.Type{
+		Name:        name,
+		Annotations: s.annos,
+		Members:     members,
+	})
+
+	return newRootState(s.r, s.lineNumber), nil
+}
+
+type typeEntityParser struct {
+	acceptName   func(name string)
+	acceptMember func(member spec.Member)
+}
+
+func (p *typeEntityParser) parseLine(line string, api *spec.ApiSpec, annos []spec.Annotation) error {
+	index := strings.Index(line, "//")
+	comment := ""
+	if index >= 0 {
+		comment = line[index+2:]
+		line = strings.TrimSpace(line[:index])
+	}
+	fields := strings.Fields(line)
+	if len(fields) == 0 {
+		return nil
+	}
+	if len(fields) == 1 {
+		p.acceptMember(spec.Member{
+			Annotations: annos,
+			Name:        fields[0],
+			Type:        fields[0],
+			IsInline:    true,
+		})
+		return nil
+	}
+	name := fields[0]
+	tp := fields[1]
+	var tag string
+	if len(fields) > 2 {
+		tag = fields[2]
+	} else {
+		tag = fmt.Sprintf("`json:\"%s\"`", util.Untitle(name))
+	}
+
+	p.acceptMember(spec.Member{
+		Annotations: annos,
+		Name:        name,
+		Type:        tp,
+		Tag:         tag,
+		Comment:     comment,
+		IsInline:    false,
+	})
+	return nil
+}
+
+func (p *typeEntityParser) setEntityName(name string) {
+	p.acceptName(name)
+}

+ 103 - 0
tools/goctl/api/parser/util.go

@@ -0,0 +1,103 @@
+package parser
+
+import (
+	"bufio"
+	"errors"
+	"regexp"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+)
+
+const (
+	// struct匹配
+	typeRegex = `(?m)(?m)(^ *type\s+[a-zA-Z][a-zA-Z0-9_-]+\s+(((struct)\s*?\{[\w\W]*?[^\{]\})|([a-zA-Z][a-zA-Z0-9_-]+)))|(^ *type\s*?\([\w\W]+\}\s*\))`
+)
+
+var (
+	emptyStrcut = errors.New("struct body not found")
+)
+
+var emptyType spec.Type
+
+func GetType(api *spec.ApiSpec, t string) spec.Type {
+	for _, tp := range api.Types {
+		if tp.Name == t {
+			return tp
+		}
+	}
+
+	return emptyType
+}
+
+func isLetterDigit(r rune) bool {
+	return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || ('0' <= r && r <= '9')
+}
+
+func isSpace(r rune) bool {
+	return r == ' ' || r == '\t'
+}
+
+func isNewline(r rune) bool {
+	return r == '\n' || r == '\r'
+}
+
+func read(r *bufio.Reader) (rune, error) {
+	ch, _, err := r.ReadRune()
+	return ch, err
+}
+
+func readLine(r *bufio.Reader) (string, error) {
+	line, _, err := r.ReadLine()
+	if err != nil {
+		return "", err
+	} else {
+		return string(line), nil
+	}
+}
+
+func skipSpaces(r *bufio.Reader) error {
+	for {
+		next, err := read(r)
+		if err != nil {
+			return err
+		}
+		if !isSpace(next) {
+			return unread(r)
+		}
+	}
+}
+
+func unread(r *bufio.Reader) error {
+	return r.UnreadRune()
+}
+
+func MatchStruct(api string) (info, structBody, service string, err error) {
+	r := regexp.MustCompile(typeRegex)
+	indexes := r.FindAllStringIndex(api, -1)
+	if len(indexes) == 0 {
+		return "", "", "", emptyStrcut
+	}
+	startIndexes := indexes[0]
+	endIndexes := indexes[len(indexes)-1]
+
+	info = api[:startIndexes[0]]
+	structBody = api[startIndexes[0]:endIndexes[len(endIndexes)-1]]
+	service = api[endIndexes[len(endIndexes)-1]:]
+
+	firstIIndex := strings.Index(info, "i")
+	if firstIIndex > 0 {
+		info = info[firstIIndex:]
+	}
+
+	lastServiceRightBraceIndex := strings.LastIndex(service, "}") + 1
+	var firstServiceIndex int
+	for index, char := range service {
+		if !isSpace(char) && !isNewline(char) {
+			firstServiceIndex = index
+			break
+		}
+	}
+	service = service[firstServiceIndex:lastServiceRightBraceIndex]
+	return
+}

+ 54 - 0
tools/goctl/api/parser/validator.go

@@ -0,0 +1,54 @@
+package parser
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+	"zero/core/stringx"
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/api/util"
+)
+
+func (p *Parser) validate(api *spec.ApiSpec) (err error) {
+	var builder strings.Builder
+	for _, tp := range api.Types {
+		if ok, name := p.validateDuplicateProperty(tp); !ok {
+			fmt.Fprintf(&builder, `duplicate property "%s" of type "%s"`+"\n", name, tp.Name)
+		}
+	}
+	if ok, info := p.validateDuplicateRouteHandler(api); !ok {
+		fmt.Fprintf(&builder, info)
+	}
+	if len(builder.String()) > 0 {
+		return errors.New(builder.String())
+	}
+	return nil
+}
+
+func (p *Parser) validateDuplicateProperty(tp spec.Type) (bool, string) {
+	var names []string
+	for _, member := range tp.Members {
+		if stringx.Contains(names, member.Name) {
+			return false, member.Name
+		} else {
+			names = append(names, member.Name)
+		}
+	}
+	return true, ""
+}
+
+func (p *Parser) validateDuplicateRouteHandler(api *spec.ApiSpec) (bool, string) {
+	var names []string
+	for _, r := range api.Service.Routes {
+		handler, ok := util.GetAnnotationValue(r.Annotations, "server", "handler")
+		if !ok {
+			return false, fmt.Sprintf("missing handler annotation for %s", r.Path)
+		}
+		if stringx.Contains(names, handler) {
+			return false, fmt.Sprintf(`duplicated handler for name "%s"`, handler)
+		} else {
+			names = append(names, handler)
+		}
+	}
+	return true, ""
+}

+ 16 - 0
tools/goctl/api/parser/vars.go

@@ -0,0 +1,16 @@
+package parser
+
+const (
+	infoDirective     = "info"
+	serviceDirective  = "service"
+	typeDirective     = "type"
+	typeStruct        = "struct"
+	at                = '@'
+	colon             = ':'
+	leftParenthesis   = '('
+	rightParenthesis  = ')'
+	leftBrace         = "{"
+	rightBrace        = '}'
+	multilineBeginTag = '>'
+	multilineEndTag   = '<'
+)

+ 143 - 0
tools/goctl/api/spec/fn.go

@@ -0,0 +1,143 @@
+package spec
+
+import (
+	"errors"
+	"regexp"
+	"strings"
+
+	"zero/core/stringx"
+	"zero/tools/goctl/util"
+)
+
+const (
+	TagKey    = "tag"
+	NameKey   = "name"
+	OptionKey = "option"
+	BodyTag   = "json"
+)
+
+var (
+	TagRe       = regexp.MustCompile(`(?P<tag>\w+):"(?P<name>[^,"]+)[,]?(?P<option>[^"]*)"`)
+	TagSubNames = TagRe.SubexpNames()
+	definedTags = []string{TagKey, NameKey, OptionKey}
+)
+
+type Attribute struct {
+	Key   string
+	value string
+}
+
+func (m Member) IsOptional() bool {
+	var option string
+
+	matches := TagRe.FindStringSubmatch(m.Tag)
+	for i := range matches {
+		name := TagSubNames[i]
+		if name == OptionKey {
+			option = matches[i]
+		}
+	}
+
+	if len(option) == 0 {
+		return false
+	}
+
+	fields := strings.Split(option, ",")
+	for _, field := range fields {
+		if field == "optional" || strings.HasPrefix(field, "default=") {
+			return true
+		}
+	}
+
+	return false
+}
+
+func (m Member) IsOmitempty() bool {
+	var option string
+
+	matches := TagRe.FindStringSubmatch(m.Tag)
+	for i := range matches {
+		name := TagSubNames[i]
+		if name == OptionKey {
+			option = matches[i]
+		}
+	}
+
+	if len(option) == 0 {
+		return false
+	}
+
+	fields := strings.Split(option, ",")
+	for _, field := range fields {
+		if field == "omitempty" {
+			return true
+		}
+	}
+
+	return false
+}
+
+func (m Member) GetAttributes() []Attribute {
+	matches := TagRe.FindStringSubmatch(m.Tag)
+	var result []Attribute
+	for i := range matches {
+		name := TagSubNames[i]
+		if stringx.Contains(definedTags, name) {
+			result = append(result, Attribute{
+				Key:   name,
+				value: matches[i],
+			})
+		}
+	}
+	return result
+}
+
+func (m Member) GetPropertyName() (string, error) {
+	attrs := m.GetAttributes()
+	for _, attr := range attrs {
+		if attr.Key == NameKey && len(attr.value) > 0 {
+			if attr.value == "-" {
+				return util.Untitle(m.Name), nil
+			}
+			return attr.value, nil
+		}
+	}
+	return "", errors.New("json property name not exist, member: " + m.Name)
+}
+
+func (m Member) GetComment() string {
+	return strings.TrimSpace(strings.Join(m.Comments, "; "))
+}
+
+func (m Member) IsBodyMember() bool {
+	if m.IsInline {
+		return true
+	}
+	attrs := m.GetAttributes()
+	for _, attr := range attrs {
+		if attr.value == BodyTag {
+			return true
+		}
+	}
+	return false
+}
+
+func (t Type) GetBodyMembers() []Member {
+	var result []Member
+	for _, member := range t.Members {
+		if member.IsBodyMember() {
+			result = append(result, member)
+		}
+	}
+	return result
+}
+
+func (t Type) GetNonBodyMembers() []Member {
+	var result []Member
+	for _, member := range t.Members {
+		if !member.IsBodyMember() {
+			result = append(result, member)
+		}
+	}
+	return result
+}

+ 131 - 0
tools/goctl/api/spec/spec.go

@@ -0,0 +1,131 @@
+package spec
+
+type (
+	Annotation struct {
+		Name       string
+		Properties map[string]string
+	}
+
+	ApiSpec struct {
+		Info    Info
+		Types   []Type
+		Service Service
+	}
+
+	Group struct {
+		Annotations []Annotation
+		Routes      []Route
+	}
+
+	Info struct {
+		Title   string
+		Desc    string
+		Version string
+		Author  string
+		Email   string
+	}
+
+	Member struct {
+		Annotations []Annotation
+		Name        string
+		// 数据类型字面值,如:string、map[int]string、[]int64、[]*User
+		Type string
+		// it can be asserted as BasicType: int、bool、
+		// PointerType: *string、*User、
+		// MapType: map[${BasicType}]interface、
+		// ArrayType:[]int、[]User、[]*User
+		// InterfaceType: interface{}
+		// Type
+		Expr interface{}
+		Tag  string
+		// Deprecated
+		Comment string // 换成标准struct中将废弃
+		// 成员尾部注释说明
+		Comments []string
+		// 成员头顶注释说明
+		Docs     []string
+		IsInline bool
+	}
+
+	Route struct {
+		Annotations  []Annotation
+		Method       string
+		Path         string
+		RequestType  Type
+		ResponseType Type
+	}
+
+	Service struct {
+		Name        string
+		Annotations []Annotation
+		Routes      []Route
+		Groups      []Group
+	}
+
+	Type struct {
+		Name        string
+		Annotations []Annotation
+		Members     []Member
+	}
+
+	// 系统预设基本数据类型
+	BasicType struct {
+		StringExpr string
+		Name       string
+	}
+	PointerType struct {
+		StringExpr string
+		// it can be asserted as BasicType: int、bool、
+		// PointerType: *string、*User、
+		// MapType: map[${BasicType}]interface、
+		// ArrayType:[]int、[]User、[]*User
+		// InterfaceType: interface{}
+		// Type
+		Star interface{}
+	}
+
+	MapType struct {
+		StringExpr string
+		// only support the BasicType
+		Key string
+		// it can be asserted as BasicType: int、bool、
+		// PointerType: *string、*User、
+		// MapType: map[${BasicType}]interface、
+		// ArrayType:[]int、[]User、[]*User
+		// InterfaceType: interface{}
+		// Type
+		Value interface{}
+	}
+	ArrayType struct {
+		StringExpr string
+		// it can be asserted as BasicType: int、bool、
+		// PointerType: *string、*User、
+		// MapType: map[${BasicType}]interface、
+		// ArrayType:[]int、[]User、[]*User
+		// InterfaceType: interface{}
+		// Type
+		ArrayType interface{}
+	}
+	InterfaceType struct {
+		StringExpr string
+		// do nothing,just for assert
+	}
+	TimeType struct {
+		StringExpr string
+	}
+	StructType struct {
+		StringExpr string
+	}
+)
+
+func (spec *ApiSpec) ContainsTime() bool {
+	for _, item := range spec.Types {
+		members := item.Members
+		for _, member := range members {
+			if _, ok := member.Expr.(*TimeType); ok {
+				return true
+			}
+		}
+	}
+	return false
+}

+ 44 - 0
tools/goctl/api/tsgen/gen.go

@@ -0,0 +1,44 @@
+package tsgen
+
+import (
+	"errors"
+	"fmt"
+
+	"zero/core/lang"
+	"zero/tools/goctl/api/parser"
+	"zero/tools/goctl/util"
+
+	"github.com/logrusorgru/aurora"
+	"github.com/urfave/cli"
+)
+
+func TsCommand(c *cli.Context) error {
+	apiFile := c.String("api")
+	dir := c.String("dir")
+	webApi := c.String("webapi")
+	caller := c.String("caller")
+	unwrapApi := c.Bool("unwrap")
+	if len(apiFile) == 0 {
+		return errors.New("missing -api")
+	}
+	if len(dir) == 0 {
+		return errors.New("missing -dir")
+	}
+
+	p, err := parser.NewParser(apiFile)
+	if err != nil {
+		return err
+	}
+	api, err := p.Parse()
+	if err != nil {
+		fmt.Println(aurora.Red("Failed"))
+		return err
+	}
+
+	lang.Must(util.MkdirIfNotExist(dir))
+	lang.Must(genHandler(dir, webApi, caller, api, unwrapApi))
+	lang.Must(genComponents(dir, api))
+
+	fmt.Println(aurora.Green("Done."))
+	return nil
+}

+ 79 - 0
tools/goctl/api/tsgen/gencomponents.go

@@ -0,0 +1,79 @@
+package tsgen
+
+import (
+	"errors"
+	"path"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	apiutil "zero/tools/goctl/api/util"
+	"zero/tools/goctl/util"
+)
+
+const (
+	componentsTemplate = `// DO NOT EDIT, generated by goctl
+
+{{.componentTypes}}
+`
+)
+
+func genComponents(dir string, api *spec.ApiSpec) error {
+	types := apiutil.GetSharedTypes(api)
+	if len(types) == 0 {
+		return nil
+	}
+
+	val, err := buildTypes(types, func(name string) (*spec.Type, error) {
+		for _, ty := range api.Types {
+			if strings.ToLower(ty.Name) == strings.ToLower(name) {
+				return &ty, nil
+			}
+		}
+		return nil, errors.New("inline type " + name + " not exist, please correct api file")
+	})
+	if err != nil {
+		return err
+	}
+
+	outputFile := apiutil.ComponentName(api) + ".ts"
+	filename := path.Join(dir, outputFile)
+	if err := util.RemoveIfExist(filename); err != nil {
+		return err
+	}
+
+	fp, created, err := apiutil.MaybeCreateFile(dir, ".", outputFile)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	t := template.Must(template.New("componentsTemplate").Parse(componentsTemplate))
+	return t.Execute(fp, map[string]string{
+		"componentTypes": val,
+	})
+}
+
+func buildTypes(types []spec.Type, inlineType func(string) (*spec.Type, error)) (string, error) {
+	var builder strings.Builder
+	first := true
+	for _, tp := range types {
+		if first {
+			first = false
+		} else {
+			builder.WriteString("\n")
+		}
+		if err := writeType(&builder, tp, func(name string) (*spec.Type, error) {
+			return inlineType(name)
+		}, func(tp string) string {
+			return ""
+		}); err != nil {
+			return "", apiutil.WrapErr(err, "Type "+tp.Name+" generate error")
+		}
+	}
+
+	return builder.String(), nil
+}

+ 214 - 0
tools/goctl/api/tsgen/genpacket.go

@@ -0,0 +1,214 @@
+package tsgen
+
+import (
+	"errors"
+	"fmt"
+	"path"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	apiutil "zero/tools/goctl/api/util"
+	"zero/tools/goctl/util"
+)
+
+const (
+	handlerTemplate = `{{.imports}}
+
+{{.types}}
+
+{{.apis}}
+`
+)
+
+func genHandler(dir, webApi, caller string, api *spec.ApiSpec, unwrapApi bool) error {
+	filename := strings.Replace(api.Service.Name, "-api", "", 1) + ".ts"
+	if err := util.RemoveIfExist(path.Join(dir, filename)); err != nil {
+		return err
+	}
+	fp, created, err := apiutil.MaybeCreateFile(dir, "", filename)
+	if err != nil {
+		return err
+	}
+	if !created {
+		return nil
+	}
+	defer fp.Close()
+
+	var localTypes []spec.Type
+	for _, route := range api.Service.Routes {
+		rts := apiutil.GetLocalTypes(api, route)
+		localTypes = append(localTypes, rts...)
+	}
+
+	var prefixForType = func(ty string) string {
+		if _, pri := primitiveType(ty); pri {
+			return ""
+		}
+		for _, item := range localTypes {
+			if util.Title(item.Name) == ty {
+				return ""
+			}
+		}
+		return packagePrefix
+	}
+
+	types, err := genTypes(localTypes, func(name string) (*spec.Type, error) {
+		for _, ty := range api.Types {
+			if strings.ToLower(ty.Name) == strings.ToLower(name) {
+				return &ty, nil
+			}
+		}
+		return nil, errors.New("inline type " + name + " not exist, please correct api file")
+	}, prefixForType)
+	if err != nil {
+		return err
+	}
+
+	imports := ""
+	if len(caller) == 0 {
+		caller = "webapi"
+	}
+	importCaller := caller
+	if unwrapApi {
+		importCaller = "{ " + importCaller + " }"
+	}
+	if len(webApi) > 0 {
+		imports += `import ` + importCaller + ` from ` + "\"" + webApi + "\""
+	}
+	shardTypes := apiutil.GetSharedTypes(api)
+	if len(shardTypes) != 0 {
+		if len(imports) > 0 {
+			imports += "\n"
+		}
+		outputFile := apiutil.ComponentName(api)
+		imports += fmt.Sprintf(`import * as components from "%s"`, "./"+outputFile)
+	}
+
+	apis, err := genApi(api, localTypes, caller, prefixForType)
+	if err != nil {
+		return err
+	}
+
+	t := template.Must(template.New("handlerTemplate").Parse(handlerTemplate))
+	return t.Execute(fp, map[string]string{
+		"webApi":  webApi,
+		"types":   strings.TrimSpace(types),
+		"imports": imports,
+		"apis":    strings.TrimSpace(apis),
+	})
+}
+
+func genTypes(localTypes []spec.Type, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) (string, error) {
+	var builder strings.Builder
+	var first bool
+
+	for _, tp := range localTypes {
+		if first {
+			first = false
+		} else {
+			fmt.Fprintln(&builder)
+		}
+		if err := writeType(&builder, tp, func(name string) (s *spec.Type, err error) {
+			return inlineType(name)
+		}, prefixForType); err != nil {
+			return "", err
+		}
+	}
+	types := builder.String()
+	return types, nil
+}
+
+func genApi(api *spec.ApiSpec, localTypes []spec.Type, caller string, prefixForType func(string) string) (string, error) {
+	var builder strings.Builder
+	for _, route := range api.Service.Routes {
+		handler, ok := apiutil.GetAnnotationValue(route.Annotations, "server", "handler")
+		if !ok {
+			return "", fmt.Errorf("missing handler annotation for route %q", route.Path)
+		}
+		handler = util.Untitle(handler)
+		handler = strings.Replace(handler, "Handler", "", 1)
+		comment := commentForRoute(route)
+		if len(comment) > 0 {
+			fmt.Fprintf(&builder, "%s\n", comment)
+		}
+		fmt.Fprintf(&builder, "export function %s(%s) {\n", handler, paramsForRoute(route, prefixForType))
+		writeIndent(&builder, 1)
+		responseGeneric := "<null>"
+		if len(route.ResponseType.Name) > 0 {
+			val, err := goTypeToTs(route.ResponseType.Name, prefixForType)
+			if err != nil {
+				return "", err
+			}
+			responseGeneric = fmt.Sprintf("<%s>", val)
+		}
+		fmt.Fprintf(&builder, `return %s.%s%s(%s)`, caller, strings.ToLower(route.Method),
+			util.Title(responseGeneric), callParamsForRoute(route))
+		builder.WriteString("\n}\n\n")
+	}
+
+	apis := builder.String()
+	return apis, nil
+}
+
+func paramsForRoute(route spec.Route, prefixForType func(string) string) string {
+	hasParams := pathHasParams(route)
+	hasBody := hasRequestBody(route)
+	rt, err := goTypeToTs(route.RequestType.Name, prefixForType)
+	if err != nil {
+		println(err.Error())
+		return ""
+	}
+	if hasParams && hasBody {
+		return fmt.Sprintf("params: %s, req: %s", rt+"Params", rt)
+	} else if hasParams {
+		return fmt.Sprintf("params: %s", rt+"Params")
+	} else if hasBody {
+		return fmt.Sprintf("req: %s", rt)
+	}
+	return ""
+}
+
+func commentForRoute(route spec.Route) string {
+	var builder strings.Builder
+	comment, _ := apiutil.GetAnnotationValue(route.Annotations, "doc", "summary")
+	builder.WriteString("/**")
+	builder.WriteString("\n * @description " + comment)
+	hasParams := pathHasParams(route)
+	hasBody := hasRequestBody(route)
+	if hasParams && hasBody {
+		builder.WriteString("\n * @param params")
+		builder.WriteString("\n * @param req")
+	} else if hasParams {
+		builder.WriteString("\n * @param params")
+	} else if hasBody {
+		builder.WriteString("\n * @param req")
+	}
+	builder.WriteString("\n */")
+	return builder.String()
+}
+
+func callParamsForRoute(route spec.Route) string {
+	hasParams := pathHasParams(route)
+	hasBody := hasRequestBody(route)
+	if hasParams && hasBody {
+		return fmt.Sprintf("%s, %s, %s", pathForRoute(route), "params", "req")
+	} else if hasParams {
+		return fmt.Sprintf("%s, %s", pathForRoute(route), "params")
+	} else if hasBody {
+		return fmt.Sprintf("%s, %s", pathForRoute(route), "req")
+	}
+	return pathForRoute(route)
+}
+
+func pathForRoute(route spec.Route) string {
+	return "\"" + route.Path + "\""
+}
+
+func pathHasParams(route spec.Route) bool {
+	return len(route.RequestType.Members) != len(route.RequestType.GetBodyMembers())
+}
+
+func hasRequestBody(route spec.Route) bool {
+	return len(route.RequestType.Name) > 0 && len(route.RequestType.GetBodyMembers()) > 0
+}

+ 167 - 0
tools/goctl/api/tsgen/util.go

@@ -0,0 +1,167 @@
+package tsgen
+
+import (
+	"fmt"
+	"io"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+	apiutil "zero/tools/goctl/api/util"
+	"zero/tools/goctl/util"
+)
+
+func writeProperty(writer io.Writer, member spec.Member, indent int, prefixForType func(string) string) error {
+	writeIndent(writer, indent)
+	ty, err := goTypeToTs(member.Type, prefixForType)
+	optionalTag := ""
+	if member.IsOptional() || member.IsOmitempty() {
+		optionalTag = "?"
+	}
+	name, err := member.GetPropertyName()
+	if err != nil {
+		return err
+	}
+	comment := member.GetComment()
+	if len(comment) > 0 {
+		comment = strings.TrimPrefix(comment, "//")
+		comment = " // " + strings.TrimSpace(comment)
+	}
+	if len(member.Docs) > 0 {
+		_, err = fmt.Fprintf(writer, "%s\n", strings.Join(member.Docs, ""))
+		writeIndent(writer, 1)
+	}
+	_, err = fmt.Fprintf(writer, "%s%s: %s%s\n", name, optionalTag, ty, comment)
+	return err
+}
+
+func writeIndent(writer io.Writer, indent int) {
+	for i := 0; i < indent; i++ {
+		fmt.Fprint(writer, "\t")
+	}
+}
+
+func goTypeToTs(tp string, prefixForType func(string) string) (string, error) {
+	if val, pri := primitiveType(tp); pri {
+		return val, nil
+	}
+	if tp == "[]byte" {
+		return "Blob", nil
+	} else if strings.HasPrefix(tp, "[][]") {
+		tys, err := apiutil.DecomposeType(tp)
+		if err != nil {
+			return "", err
+		}
+		if len(tys) == 0 {
+			return "", fmt.Errorf("%s tp parse error", tp)
+		}
+		innerType, err := goTypeToTs(tys[0], prefixForType)
+		if err != nil {
+			return "", err
+		}
+		return fmt.Sprintf("Array<Array<%s>>", innerType), nil
+	} else if strings.HasPrefix(tp, "[]") {
+		tys, err := apiutil.DecomposeType(tp)
+		if err != nil {
+			return "", err
+		}
+		if len(tys) == 0 {
+			return "", fmt.Errorf("%s tp parse error", tp)
+		}
+		innerType, err := goTypeToTs(tys[0], prefixForType)
+		if err != nil {
+			return "", err
+		}
+		return fmt.Sprintf("Array<%s>", innerType), nil
+	} else if strings.HasPrefix(tp, "map") {
+		tys, err := apiutil.DecomposeType(tp)
+		if err != nil {
+			return "", err
+		}
+		if len(tys) != 2 {
+			return "", fmt.Errorf("%s tp parse error", tp)
+		}
+		innerType, err := goTypeToTs(tys[1], prefixForType)
+		if err != nil {
+			return "", err
+		}
+		return fmt.Sprintf("{ [key: string]: %s }", innerType), nil
+	}
+	return addPrefixIfNeed(util.Title(tp), prefixForType), nil
+}
+
+func addPrefixIfNeed(tp string, prefixForType func(string) string) string {
+	if val, pri := primitiveType(tp); pri {
+		return val
+	}
+	tp = strings.Replace(tp, "*", "", 1)
+	return prefixForType(tp) + util.Title(tp)
+}
+
+func primitiveType(tp string) (string, bool) {
+	switch tp {
+	case "string":
+		return "string", true
+	case "int", "int8", "int32", "int64":
+		return "number", true
+	case "float", "float32", "float64":
+		return "number", true
+	case "bool":
+		return "boolean", true
+	case "[]byte":
+		return "Blob", true
+	case "interface{}":
+		return "any", true
+	}
+	return "", false
+}
+
+func writeType(writer io.Writer, tp spec.Type, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) error {
+	fmt.Fprintf(writer, "export interface %s {\n", util.Title(tp.Name))
+	if err := genMembers(writer, tp, false, inlineType, prefixForType); err != nil {
+		return err
+	}
+	fmt.Fprintf(writer, "}\n")
+	err := genParamsTypesIfNeed(writer, tp, inlineType, prefixForType)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func genParamsTypesIfNeed(writer io.Writer, tp spec.Type, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) error {
+	members := tp.GetNonBodyMembers()
+	if len(members) == 0 {
+		return nil
+	}
+	fmt.Fprintf(writer, "\n")
+	fmt.Fprintf(writer, "export interface %sParams {\n", util.Title(tp.Name))
+	if err := genMembers(writer, tp, true, inlineType, prefixForType); err != nil {
+		return err
+	}
+	fmt.Fprintf(writer, "}\n")
+	return nil
+}
+
+func genMembers(writer io.Writer, tp spec.Type, isParam bool, inlineType func(string) (*spec.Type, error), prefixForType func(string) string) error {
+	members := tp.GetBodyMembers()
+	if isParam {
+		members = tp.GetNonBodyMembers()
+	}
+	for _, member := range members {
+		if member.IsInline {
+			// 获取inline类型的成员然后添加到type中
+			it, err := inlineType(strings.TrimPrefix(member.Type, "*"))
+			if err != nil {
+				return err
+			}
+			if err := genMembers(writer, *it, isParam, inlineType, prefixForType); err != nil {
+				return err
+			}
+			continue
+		}
+		if err := writeProperty(writer, member, 1, prefixForType); err != nil {
+			return apiutil.WrapErr(err, " type "+tp.Name)
+		}
+	}
+	return nil
+}

+ 5 - 0
tools/goctl/api/tsgen/vars.go

@@ -0,0 +1,5 @@
+package tsgen
+
+const (
+	packagePrefix = "components."
+)

+ 13 - 0
tools/goctl/api/util/annotation.go

@@ -0,0 +1,13 @@
+package util
+
+import "zero/tools/goctl/api/spec"
+
+func GetAnnotationValue(annos []spec.Annotation, key, field string) (string, bool) {
+	for _, anno := range annos {
+		if anno.Name == key {
+			value, ok := anno.Properties[field]
+			return value, ok
+		}
+	}
+	return "", false
+}

+ 107 - 0
tools/goctl/api/util/case.go

@@ -0,0 +1,107 @@
+package util
+
+func IsUpperCase(r rune) bool {
+	if r >= 'A' && r <= 'Z' {
+		return true
+	}
+	return false
+}
+
+func IsLowerCase(r rune) bool {
+	if r >= 'a' && r <= 'z' {
+		return true
+	}
+	return false
+}
+
+func ToSnakeCase(s string) string {
+	out := []rune{}
+	for index, r := range s {
+		if index == 0 {
+			out = append(out, ToLowerCase(r))
+			continue
+		}
+
+		if IsUpperCase(r) && index != 0 {
+			if IsLowerCase(rune(s[index-1])) {
+				out = append(out, '_', ToLowerCase(r))
+				continue
+			}
+			if index < len(s)-1 && IsLowerCase(rune(s[index+1])) {
+				out = append(out, '_', ToLowerCase(r))
+				continue
+			}
+			out = append(out, ToLowerCase(r))
+			continue
+		}
+		out = append(out, r)
+	}
+	return string(out)
+}
+
+func ToCamelCase(s string) string {
+	s = ToLower(s)
+	out := []rune{}
+	for index, r := range s {
+		if r == '_' {
+			continue
+		}
+		if index == 0 {
+			out = append(out, ToUpperCase(r))
+			continue
+		}
+
+		if index > 0 && s[index-1] == '_' {
+			out = append(out, ToUpperCase(r))
+			continue
+		}
+
+		out = append(out, r)
+	}
+	return string(out)
+}
+
+func ToLowerCase(r rune) rune {
+	dx := 'A' - 'a'
+	if IsUpperCase(r) {
+		return r - dx
+	}
+	return r
+}
+func ToUpperCase(r rune) rune {
+	dx := 'A' - 'a'
+	if IsLowerCase(r) {
+		return r + dx
+	}
+	return r
+}
+
+func ToLower(s string) string {
+	out := []rune{}
+	for _, r := range s {
+		out = append(out, ToLowerCase(r))
+	}
+	return string(out)
+}
+
+func ToUpper(s string) string {
+	out := []rune{}
+	for _, r := range s {
+		out = append(out, ToUpperCase(r))
+	}
+	return string(out)
+}
+
+func LowerFirst(s string) string {
+	if len(s) == 0 {
+		return s
+	}
+	return ToLower(s[:1]) + s[1:]
+}
+
+func UpperFirst(s string) string {
+	if len(s) == 0 {
+		return s
+	}
+	return ToUpper(s[:1]) + s[1:]
+}

+ 58 - 0
tools/goctl/api/util/tag.go

@@ -0,0 +1,58 @@
+package util
+
+import (
+	"strconv"
+	"strings"
+)
+
+func TagLookup(tag, key string) (value string, ok bool) {
+	tag = strings.Replace(tag, "`", "", -1)
+	for tag != "" {
+		// Skip leading space.
+		i := 0
+		for i < len(tag) && tag[i] == ' ' {
+			i++
+		}
+		tag = tag[i:]
+		if tag == "" {
+			break
+		}
+
+		// Scan to colon. A space, a quote or a control character is a syntax error.
+		// Strictly speaking, control chars include the range [0x7f, 0x9f], not just
+		// [0x00, 0x1f], but in practice, we ignore the multi-byte control characters
+		// as it is simpler to inspect the tag's bytes than the tag's runes.
+		i = 0
+		for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f {
+			i++
+		}
+		if i == 0 || i+1 >= len(tag) || tag[i] != ':' || tag[i+1] != '"' {
+			break
+		}
+		name := string(tag[:i])
+		tag = tag[i+1:]
+
+		// Scan quoted string to find value.
+		i = 1
+		for i < len(tag) && tag[i] != '"' {
+			if tag[i] == '\\' {
+				i++
+			}
+			i++
+		}
+		if i >= len(tag) {
+			break
+		}
+		qvalue := string(tag[:i+1])
+		tag = tag[i+1:]
+
+		if key == name {
+			value, err := strconv.Unquote(qvalue)
+			if err != nil {
+				break
+			}
+			return value, true
+		}
+	}
+	return "", false
+}

+ 159 - 0
tools/goctl/api/util/types.go

@@ -0,0 +1,159 @@
+package util
+
+import (
+	"fmt"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+)
+
+func DecomposeType(t string) (result []string, err error) {
+	add := func(tp string) error {
+		ret, err := DecomposeType(tp)
+		if err != nil {
+			return err
+		}
+
+		result = append(result, ret...)
+		return nil
+	}
+	if strings.HasPrefix(t, "map") {
+		t = strings.ReplaceAll(t, "map", "")
+		if t[0] == '[' {
+			pos := strings.Index(t, "]")
+			if pos > 1 {
+				if err = add(t[1:pos]); err != nil {
+					return
+				}
+				if len(t) > pos+1 {
+					err = add(t[pos+1:])
+					return
+				}
+			}
+		}
+	} else if strings.HasPrefix(t, "[]") {
+		if len(t) > 2 {
+			err = add(t[2:])
+			return
+		}
+	} else if strings.HasPrefix(t, "*") {
+		err = add(t[1:])
+		return
+	} else {
+		result = append(result, t)
+		return
+	}
+
+	err = fmt.Errorf("bad type %q", t)
+	return
+}
+
+func GetAllTypes(api *spec.ApiSpec, route spec.Route) []spec.Type {
+	var rts []spec.Type
+	types := api.Types
+	getTypeRecursive(route.RequestType, types, &rts)
+	getTypeRecursive(route.ResponseType, types, &rts)
+	return rts
+}
+
+func GetLocalTypes(api *spec.ApiSpec, route spec.Route) []spec.Type {
+	sharedTypes := GetSharedTypes(api)
+	isSharedType := func(ty spec.Type) bool {
+		for _, item := range sharedTypes {
+			if item.Name == ty.Name {
+				return true
+			}
+		}
+		return false
+	}
+
+	var rts = GetAllTypes(api, route)
+
+	var result []spec.Type
+	for _, item := range rts {
+		if !isSharedType(item) {
+			result = append(result, item)
+		}
+	}
+	return result
+}
+
+func getTypeRecursive(ty spec.Type, allTypes []spec.Type, result *[]spec.Type) {
+	isCustomType := func(name string) (*spec.Type, bool) {
+		for _, item := range allTypes {
+			if item.Name == name {
+				return &item, true
+			}
+		}
+		return nil, false
+	}
+	if len(ty.Name) > 0 {
+		*result = append(*result, ty)
+	}
+	for _, member := range ty.Members {
+		decomposedItems, _ := DecomposeType(member.Type)
+		if len(decomposedItems) == 0 {
+			continue
+		}
+		var customTypes []spec.Type
+		for _, item := range decomposedItems {
+			c, e := isCustomType(item)
+			if e {
+				customTypes = append(customTypes, *c)
+			}
+		}
+		for _, ty := range customTypes {
+			hasAppend := false
+			for _, item := range *result {
+				if ty.Name == item.Name {
+					hasAppend = true
+					break
+				}
+
+			}
+			if !hasAppend {
+				getTypeRecursive(ty, allTypes, result)
+			}
+		}
+	}
+}
+
+func GetSharedTypes(api *spec.ApiSpec) []spec.Type {
+	types := api.Types
+	var result []spec.Type
+	var container []spec.Type
+	hasInclude := func(all []spec.Type, ty spec.Type) bool {
+		for _, item := range all {
+			if item.Name == ty.Name {
+				return true
+			}
+		}
+		return false
+	}
+	for _, route := range api.Service.Routes {
+		var rts []spec.Type
+		getTypeRecursive(route.RequestType, types, &rts)
+		getTypeRecursive(route.ResponseType, types, &rts)
+		for _, item := range rts {
+			if len(item.Name) == 0 {
+				continue
+			}
+			if hasInclude(container, item) {
+				hasAppend := false
+				for _, r := range result {
+					if item.Name == r.Name {
+						hasAppend = true
+						break
+					}
+
+				}
+				if !hasAppend {
+					result = append(result, item)
+				}
+			} else {
+				container = append(container, item)
+			}
+		}
+	}
+	return result
+}

+ 74 - 0
tools/goctl/api/util/util.go

@@ -0,0 +1,74 @@
+package util
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"path"
+	"strings"
+	"zero/tools/goctl/api/spec"
+
+	"zero/core/lang"
+	"zero/tools/goctl/util"
+)
+
+func MaybeCreateFile(dir, subdir, file string) (fp *os.File, created bool, err error) {
+	lang.Must(util.MkdirIfNotExist(path.Join(dir, subdir)))
+	fpath := path.Join(dir, subdir, file)
+	if util.FileExists(fpath) {
+		fmt.Printf("%s exists, ignored generation\n", fpath)
+		return nil, false, nil
+	}
+
+	fp, err = util.CreateIfNotExist(fpath)
+	created = err == nil
+	return
+}
+
+func ClearAndOpenFile(fpath string) (*os.File, error) {
+	f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_TRUNC, 0600)
+
+	_, err = f.WriteString("")
+	if err != nil {
+		return nil, err
+	}
+	return f, nil
+}
+
+func WrapErr(err error, message string) error {
+	return errors.New(message + ", " + err.Error())
+}
+
+func Copy(src, dst string) (int64, error) {
+	sourceFileStat, err := os.Stat(src)
+	if err != nil {
+		return 0, err
+	}
+
+	if !sourceFileStat.Mode().IsRegular() {
+		return 0, fmt.Errorf("%s is not a regular file", src)
+	}
+
+	source, err := os.Open(src)
+	if err != nil {
+		return 0, err
+	}
+	defer source.Close()
+
+	destination, err := os.Create(dst)
+	if err != nil {
+		return 0, err
+	}
+	defer destination.Close()
+	nBytes, err := io.Copy(destination, source)
+	return nBytes, err
+}
+
+func ComponentName(api *spec.ApiSpec) string {
+	name := api.Service.Name
+	if strings.HasSuffix(name, "-api") {
+		return name[:len(name)-4] + "Components"
+	}
+	return name + "Components"
+}

+ 29 - 0
tools/goctl/api/validate/validate.go

@@ -0,0 +1,29 @@
+package validate
+
+import (
+	"errors"
+	"fmt"
+
+	"zero/tools/goctl/api/parser"
+
+	"github.com/logrusorgru/aurora"
+	"github.com/urfave/cli"
+)
+
+func GoValidateApi(c *cli.Context) error {
+	apiFile := c.String("api")
+
+	if len(apiFile) == 0 {
+		return errors.New("missing -api")
+	}
+
+	p, err := parser.NewParser(apiFile)
+	if err != nil {
+		return err
+	}
+	_, err = p.Parse()
+	if err == nil {
+		fmt.Println(aurora.Green("api format ok"))
+	}
+	return err
+}

+ 77 - 0
tools/goctl/configgen/genconfigjson.go

@@ -0,0 +1,77 @@
+package configgen
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"text/template"
+
+	"github.com/logrusorgru/aurora"
+	"github.com/urfave/cli"
+	"zero/tools/goctl/vars"
+)
+
+const configTemplate = `package main
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"os"
+	"{{.import}}"
+)
+
+func main() {
+	var c config.Config
+	template, err := json.MarshalIndent(c, "", "    ")
+	if err != nil {
+		panic(err)
+	}
+	err = ioutil.WriteFile("config.json", template, os.ModePerm)
+	if err != nil {
+		panic(err)
+	}
+}
+`
+
+func GenConfigCommand(c *cli.Context) error {
+	path, err := filepath.Abs(c.String("path"))
+	if err != nil {
+		return errors.New("abs failed: " + c.String("path"))
+	}
+	xi := strings.Index(path, vars.ProjectName)
+	if xi <= 0 {
+		return errors.New("path should the absolute path of config go file")
+	}
+	path = strings.TrimSuffix(path, "/config.go")
+	location := path + "/tmp"
+	err = os.MkdirAll(location, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	goPath := filepath.Join(location, "config.go")
+	fp, err := os.Create(goPath)
+	if err != nil {
+		return err
+	}
+	defer fp.Close()
+	defer os.RemoveAll(location)
+
+	t := template.Must(template.New("template").Parse(configTemplate))
+	if err := t.Execute(fp, map[string]string{
+		"import": path[xi:],
+	}); err != nil {
+		return err
+	}
+
+	cmd := exec.Command("go", "run", goPath)
+	_, err = cmd.Output()
+	if err != nil {
+		return err
+	}
+	fmt.Println(aurora.Green("Done."))
+	return nil
+}

+ 23 - 0
tools/goctl/docker/docker.go

@@ -0,0 +1,23 @@
+package docker
+
+import (
+	"errors"
+
+	"zero/tools/goctl/gen"
+
+	"github.com/urfave/cli"
+)
+
+func DockerCommand(c *cli.Context) error {
+	goFile := c.String("go")
+	namespace := c.String("namespace")
+	if len(goFile) == 0 || len(namespace) == 0 {
+		return errors.New("-go and -namespace can't be empty")
+	}
+
+	if err := gen.GenerateDockerfile(goFile, "-f", "etc/config.json"); err != nil {
+		return err
+	}
+
+	return gen.GenerateMakefile(goFile, namespace)
+}

+ 30 - 0
tools/goctl/example/rec.proto

@@ -0,0 +1,30 @@
+syntax = "proto3";
+
+package recommendservice;
+
+message RecArticle {
+    int64 id = 1;
+}
+
+message RecommendRequest {
+    // the id of the request user.
+    int64 uid = 1;
+    // how many top ranked article for this user.
+    int32 topk = 2;
+    // current hour
+    int32 hour = 3;
+    // current minute
+    int32 minute = 4;
+    // the article list.
+    repeated RecArticle articles = 5;
+}
+
+message RecommendResponse {
+    repeated int64 articles = 1;
+}
+
+service RecommendService {
+    // the method to get the topk performers for this user.
+    rpc recommend1(RecommendRequest) returns (RecommendResponse);
+    rpc recommend2(RecommendRequest) returns (RecommendResponse);
+}

+ 20 - 0
tools/goctl/feature/feature.go

@@ -0,0 +1,20 @@
+package feature
+
+import (
+	"fmt"
+
+	"github.com/logrusorgru/aurora"
+	"github.com/urfave/cli"
+)
+
+var feature = `
+1、新增对rpc错误转换处理
+  1.1、目前暂时仅处理not found 和 unknown错误
+2、增加feature命令支持,详细使用请通过命令[goctl -feature]查看
+`
+
+func Feature(c *cli.Context) error {
+	fmt.Println(aurora.Blue("\nFEATURE:"))
+	fmt.Println(aurora.Blue(feature))
+	return nil
+}

+ 36 - 0
tools/goctl/gen/dockerfile.go

@@ -0,0 +1,36 @@
+package gen
+
+import (
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/util"
+	"zero/tools/goctl/vars"
+)
+
+func GenerateDockerfile(goFile string, args ...string) error {
+	relPath, err := util.PathFromGoSrc()
+	if err != nil {
+		return err
+	}
+
+	out, err := util.CreateIfNotExist("Dockerfile")
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+
+	var builder strings.Builder
+	for _, arg := range args {
+		builder.WriteString(`, "` + arg + `"`)
+	}
+
+	t := template.Must(template.New("dockerfile").Parse(dockerTemplate))
+	return t.Execute(out, map[string]string{
+		"projectName": vars.ProjectName,
+		"goRelPath":   relPath,
+		"goFile":      goFile,
+		"exeFile":     util.FileNameWithoutExt(goFile),
+		"argument":    builder.String(),
+	})
+}

+ 52 - 0
tools/goctl/gen/makefile.go

@@ -0,0 +1,52 @@
+package gen
+
+import (
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/util"
+)
+
+func GenerateMakefile(goFile, namespace string) error {
+	relPath, err := util.PathFromGoSrc()
+	if err != nil {
+		return err
+	}
+
+	movePath, err := getMovePath()
+	if err != nil {
+		return err
+	}
+
+	out, err := util.CreateIfNotExist("Makefile")
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+
+	t := template.Must(template.New("makefile").Parse(makefileTemplate))
+	return t.Execute(out, map[string]string{
+		"rootRelPath": movePath,
+		"relPath":     relPath,
+		"exeFile":     util.FileNameWithoutExt(goFile),
+		"namespace":   namespace,
+	})
+}
+
+func getMovePath() (string, error) {
+	relPath, err := util.PathFromGoSrc()
+	if err != nil {
+		return "", err
+	}
+
+	var builder strings.Builder
+	for range strings.Split(relPath, "/") {
+		builder.WriteString("../")
+	}
+
+	if move := builder.String(); len(move) == 0 {
+		return ".", nil
+	} else {
+		return move, nil
+	}
+}

+ 44 - 0
tools/goctl/gen/template.go

@@ -0,0 +1,44 @@
+package gen
+
+const (
+	dockerTemplate = `FROM golang:alpine AS builder
+
+LABEL stage=gobuilder
+
+ENV CGO_ENABLED 0
+ENV GOOS linux
+ENV GOPROXY https://goproxy.cn,direct
+
+WORKDIR $GOPATH/src/{{.projectName}}
+COPY . .
+RUN go build -ldflags="-s -w" -o /app/{{.exeFile}} {{.goRelPath}}/{{.goFile}}
+
+
+FROM alpine
+
+RUN apk update --no-cache
+RUN apk add --no-cache ca-certificates
+RUN apk add --no-cache tzdata
+ENV TZ Asia/Shanghai
+
+WORKDIR /app
+COPY --from=builder /app/{{.exeFile}} /app/{{.exeFile}}
+
+CMD ["./{{.exeFile}}"{{.argument}}]
+`
+
+	makefileTemplate = `version := v$(shell /bin/date "+%y%m%d%H%M%S")
+
+build:
+	docker pull alpine
+	docker pull golang:alpine
+	cd $(GOPATH)/src/xiao && docker build -t registry.cn-hangzhou.aliyuncs.com/xapp/{{.exeFile}}:$(version) . -f {{.relPath}}/Dockerfile
+	docker image prune --filter label=stage=gobuilder -f
+
+push: build
+	docker push registry.cn-hangzhou.aliyuncs.com/xapp/{{.exeFile}}:$(version)
+
+deploy: push
+	kubectl -n {{.namespace}} set image deployment/{{.exeFile}}-deployment {{.exeFile}}=registry-vpc.cn-hangzhou.aliyuncs.com/xapp/{{.exeFile}}:$(version)
+`
+)

+ 369 - 0
tools/goctl/goctl.go

@@ -0,0 +1,369 @@
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"os/user"
+	"path"
+	"path/filepath"
+	"time"
+
+	"zero/core/conf"
+	"zero/core/hash"
+	"zero/core/lang"
+	"zero/core/logx"
+	"zero/core/mapreduce"
+	"zero/core/stringx"
+	"zero/tools/goctl/api/apigen"
+	"zero/tools/goctl/api/dartgen"
+	"zero/tools/goctl/api/docgen"
+	"zero/tools/goctl/api/format"
+	"zero/tools/goctl/api/gogen"
+	"zero/tools/goctl/api/javagen"
+	"zero/tools/goctl/api/tsgen"
+	"zero/tools/goctl/api/validate"
+	"zero/tools/goctl/configgen"
+	"zero/tools/goctl/docker"
+	"zero/tools/goctl/feature"
+	"zero/tools/goctl/model/mongomodel"
+	"zero/tools/goctl/util"
+
+	"github.com/logrusorgru/aurora"
+	"github.com/urfave/cli"
+)
+
+const (
+	autoUpdate     = "GOCTL_AUTO_UPDATE"
+	configFile     = ".goctl"
+	configTemplate = `url = http://47.97.184.41:7777/`
+	toolName       = "goctl"
+)
+
+var (
+	BuildTime = "not set"
+	commands  = []cli.Command{
+		{
+			Name:  "api",
+			Usage: "generate api related files",
+			Flags: []cli.Flag{
+				cli.StringFlag{
+					Name:  "o",
+					Usage: "the output api file",
+				},
+			},
+			Action: apigen.ApiCommand,
+			Subcommands: []cli.Command{
+				{
+					Name:  "format",
+					Usage: "format api files",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "dir",
+							Usage: "the format target dir",
+						},
+						cli.BoolFlag{
+							Name:  "p",
+							Usage: "print result to console",
+						},
+						cli.BoolFlag{
+							Name:     "iu",
+							Usage:    "ignore update",
+							Required: false,
+						},
+					},
+					Action: format.GoFormatApi,
+				},
+				{
+					Name:  "validate",
+					Usage: "validate api file",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "api",
+							Usage: "validate target api file",
+						},
+					},
+					Action: validate.GoValidateApi,
+				},
+				{
+					Name:  "doc",
+					Usage: "generate doc files",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "dir",
+							Usage: "the target dir",
+						},
+					},
+					Action: docgen.DocCommand,
+				},
+				{
+					Name:  "go",
+					Usage: "generate go files for provided api in yaml file",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "dir",
+							Usage: "the target dir",
+						},
+						cli.StringFlag{
+							Name:  "api",
+							Usage: "the api file",
+						},
+					},
+					Action: gogen.GoCommand,
+				},
+				{
+					Name:  "java",
+					Usage: "generate java files for provided api in api file",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "dir",
+							Usage: "the target dir",
+						},
+						cli.StringFlag{
+							Name:  "api",
+							Usage: "the api file",
+						},
+					},
+					Action: javagen.JavaCommand,
+				},
+				{
+					Name:  "ts",
+					Usage: "generate ts files for provided api in api file",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "dir",
+							Usage: "the target dir",
+						},
+						cli.StringFlag{
+							Name:  "api",
+							Usage: "the api file",
+						},
+						cli.StringFlag{
+							Name:     "webapi",
+							Usage:    "the web api file path",
+							Required: false,
+						},
+						cli.StringFlag{
+							Name:     "caller",
+							Usage:    "the web api caller",
+							Required: false,
+						},
+						cli.BoolFlag{
+							Name:     "unwrap",
+							Usage:    "unwrap the webapi caller for import",
+							Required: false,
+						},
+					},
+					Action: tsgen.TsCommand,
+				},
+				{
+					Name:  "dart",
+					Usage: "generate dart files for provided api in api file",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "dir",
+							Usage: "the target dir",
+						},
+						cli.StringFlag{
+							Name:  "api",
+							Usage: "the api file",
+						},
+					},
+					Action: dartgen.DartCommand,
+				},
+			},
+		},
+		{
+			Name:  "docker",
+			Usage: "generate Dockerfile and Makefile",
+			Flags: []cli.Flag{
+				cli.StringFlag{
+					Name:  "go",
+					Usage: "the file that contains main function",
+				},
+				cli.StringFlag{
+					Name:  "namespace, n",
+					Usage: "which namespace of kubernetes to deploy the service",
+				},
+			},
+			Action: docker.DockerCommand,
+		},
+		{
+			Name:  "model",
+			Usage: "generate sql model",
+			Flags: []cli.Flag{
+				cli.StringFlag{
+					Name:  "config, c",
+					Usage: "the file that contains main function",
+				},
+				cli.StringFlag{
+					Name:  "dir, d",
+					Usage: "the target dir",
+				},
+			},
+			Subcommands: []cli.Command{
+				{
+					Name:  "mongo",
+					Usage: "generate mongoModel files for provided somemongo.go in go file",
+					Flags: []cli.Flag{
+						cli.StringFlag{
+							Name:  "src, s",
+							Usage: "the src file",
+						},
+						cli.StringFlag{
+							Name:  "cache",
+							Usage: "need cache code([yes/no] default value is no)",
+						},
+					},
+					Action: mongomodel.ModelCommond,
+				},
+			},
+		},
+		{
+			Name:  "config",
+			Usage: "generate config json",
+			Flags: []cli.Flag{
+				cli.StringFlag{
+					Name:  "path, p",
+					Usage: "the target config go file",
+				},
+			},
+			Action: configgen.GenConfigCommand,
+		},
+		{
+			Name:   "feature",
+			Usage:  "the features of the latest version",
+			Action: feature.Feature,
+		},
+	}
+)
+
+func genConfigFile(file string) error {
+	return ioutil.WriteFile(file, []byte(configTemplate), 0600)
+}
+
+func getAbsFile() (string, error) {
+	exe, err := os.Executable()
+	if err != nil {
+		return "", err
+	}
+
+	dir, err := filepath.Abs(filepath.Dir(exe))
+	if err != nil {
+		return "", err
+	}
+
+	return path.Join(dir, filepath.Base(os.Args[0])), nil
+}
+
+func getFilePerm(file string) (os.FileMode, error) {
+	info, err := os.Stat(file)
+	if err != nil {
+		return 0, err
+	}
+
+	return info.Mode(), nil
+}
+
+func update() {
+	usr, err := user.Current()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	absConfigFile := path.Join(usr.HomeDir, configFile)
+	if !util.FileExists(absConfigFile) {
+		if err := genConfigFile(absConfigFile); err != nil {
+			fmt.Println(err)
+			return
+		}
+	}
+
+	props, err := conf.LoadProperties(absConfigFile)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	u, err := url.Parse(props.GetString("url"))
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	u.Path = path.Join(u.Path, toolName)
+	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	file, err := getAbsFile()
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	content, err := ioutil.ReadFile(file)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	req.Header.Set("Content-Md5", hash.Md5Hex(content))
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer resp.Body.Close()
+
+	mode, err := getFilePerm(file)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	content, err = ioutil.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	switch resp.StatusCode {
+	case http.StatusOK:
+		if err := ioutil.WriteFile(file, content, mode); err != nil {
+			fmt.Println(err)
+		}
+	}
+}
+
+func main() {
+	logx.Disable()
+
+	done := make(chan lang.PlaceholderType)
+	mapreduce.FinishVoid(func() {
+		if os.Getenv(autoUpdate) != "off" && !stringx.Contains(os.Args, "-iu") {
+			update()
+		}
+		close(done)
+	}, func() {
+		app := cli.NewApp()
+		app.Usage = "a cli tool to generate code"
+		app.Version = BuildTime
+		app.Commands = commands
+		// cli already print error messages
+		if err := app.Run(os.Args); err != nil {
+			fmt.Println("error:", err)
+		}
+	}, func() {
+		select {
+		case <-done:
+		case <-time.After(time.Second):
+			fmt.Println(aurora.Yellow("Updating goctl, please wait..."))
+		}
+	})
+}

+ 266 - 0
tools/goctl/goctl.md

@@ -0,0 +1,266 @@
+# goctl使用说明
+
+## goctl用途
+* 定义api请求
+* 根据定义的api自动生成golang(后端), java(iOS & Android), typescript(web & 晓程序),dart(flutter)
+* 生成MySQL CURD (https://goctl.xiaoheiban.cn)
+*  生成MongoDB CURD (https://goctl.xiaoheiban.cn)
+
+## goctl使用说明
+#### goctl参数说明
+
+  `goctl api [go/java/ts] [-api user/user.api] [-dir ./src]`
+  
+  > api 后面接生成的语言,现支持go/java/typescript
+  
+  > -api 自定义api所在路径
+  
+  > -dir 自定义生成目录
+  
+#### 保持goctl总是最新版
+
+  第一次运行会在~/.goctl里增加下面两行:
+
+  ```
+  url = http://47.97.184.41:7777/
+  ```
+
+#### API 语法说明
+
+```
+info(
+    title: doc title
+    desc: >
+    doc description first part,
+    doc description second part<
+    version: 1.0
+)
+
+type int userType
+
+type user struct {
+	name string `json:"user"` // 用户姓名
+}
+
+type student struct {
+	name string `json:"name"` // 学生姓名
+}
+
+type teacher struct {
+}
+
+type (
+	address struct {
+		city string `json:"city"` // 城市
+	}
+
+	innerType struct {
+		image string `json:"image"`
+	}
+
+	createRequest struct {
+		innerType
+		name    string    `form:"name"`         // niha
+		age     int       `form:"age,optional"` // nihaod
+		address []address `json:"address,optional"`
+	}
+
+	getRequest struct {
+		name string `path:"name"`
+		age  int    `form:"age,optional"`
+	}
+
+	getResponse struct {
+		code    int     `json:"code"`
+		desc    string  `json:"desc,omitempty"`
+		address address `json:"address"`
+		service int     `json:"service"`
+	}
+)
+
+service user-api {
+    @doc(
+        summary: user title
+        desc: >
+        user description first part,
+        user description second part,
+        user description second line
+    )
+    @server(
+        handler: GetUserHandler
+        folder: user
+    )
+    get /api/user/:name(getRequest) returns(getResponse)
+
+    @server(
+        handler: CreateUserHandler
+        folder: user
+    )
+    post /api/users/create(createRequest)
+}
+
+@server(
+    jwt: Auth
+    folder: profile
+)
+service user-api {
+    @doc(summary: user title)
+    @server(
+        handler: GetProfileHandler
+    )
+    get /api/profile/:name(getRequest) returns(getResponse)
+
+    @server(
+        handler: CreateProfileHandler
+    )
+    post /api/profile/create(createRequest)
+}
+
+service user-api {
+    @doc(summary: desc in one line)
+    @server(
+        handler: PingHandler
+    )
+    head /api/ping()
+}
+```
+1. info部分:描述了api基本信息,比如Auth,api是哪个用途。
+2. type部分:type类型声明和golang语法兼容。
+3. service部分:service代表一组服务,一个服务可以由多组名称相同的service组成,可以针对每一组service配置jwt和auth认证,另外通过folder属性可以指定service生成所在子目录。
+   service里面包含api路由,比如上面第一组service的第一个路由,doc用来描述此路由的用途,GetProfileHandler表示处理这个路由的handler,
+   `get /api/profile/:name(getRequest) returns(getResponse)` 中get代表api的请求方式(get/post/put/delete), `/api/profile/:name` 描述了路由path,`:name`通过
+   请求getRequest里面的属性赋值,getResponse为返回的结构体,这两个类型都定义在2描述的类型中。
+
+#### api vscode插件
+开发者可以在vscode中搜索goctl的api插件,它提供了api语法高亮,语法检测和格式化相关功能。
+
+ 1. 支持语法高亮和类型导航。
+ 2. 语法检测,格式化api会自动检测api编写错误地方,用vscode默认的格式化快捷键(option+command+F)或者自定义的也可以。
+ 3. 格式化(option+command+F),类似代码格式化,统一样式支持。
+
+#### 根据定义好的api文件生成golang代码
+
+  命令如下:  
+  `goctl api go -api user/user.api -dir user`
+
+  ```
+
+	.
+    ├── internal
+    │   ├── config
+    │   │   └── config.go
+    │   ├── handler
+    │   │   ├── pinghandler.go
+    │   │   ├── profile
+    │   │   │   ├── createprofilehandler.go
+    │   │   │   └── getprofilehandler.go
+    │   │   ├── routes.go
+    │   │   └── user
+    │   │       ├── createuserhandler.go
+    │   │       └── getuserhandler.go
+    │   ├── logic
+    │   │   ├── pinglogic.go
+    │   │   ├── profile
+    │   │   │   ├── createprofilelogic.go
+    │   │   │   └── getprofilelogic.go
+    │   │   └── user
+    │   │       ├── createuserlogic.go
+    │   │       └── getuserlogic.go
+    │   ├── svc
+    │   │   └── servicecontext.go
+    │   └── types
+    │       └── types.go
+    └── user.go
+
+  ```
+  生成的代码可以直接跑,有几个地方需要改:
+  
+  * 在`servicecontext.go`里面增加需要传递给logic的一些资源,比如mysql, redis,rpc等
+  * 在定义的get/post/put/delete等请求的handler和logic里增加处理业务逻辑的代码
+
+#### 根据定义好的api文件生成java代码
+	`goctl api java -api user/user.api -dir ./src`
+
+#### 根据定义好的api文件生成typescript代码
+	`goctl api ts -api user/user.api -dir ./src -webapi ***`
+
+	ts需要指定webapi所在目录
+	
+#### 根据定义好的api文件生成Dart代码
+	`goctl api dart -api user/user.api -dir ./src`
+
+## 根据定义好的简单go文件生成mongo代码文件(仅限golang使用)  
+    `goctl model mongo -src {{yourDir}}/xiao/service/xhb/user/model/usermodel.go -cache yes`
+    
+    -src需要提供简单的usermodel.go文件,里面只需要提供一个结构体即可
+    -cache 控制是否需要缓存 yes=需要 no=不需要
+    src 示例代码如下
+  ```
+    package model
+    
+    type User struct {
+    	Name string `o:"find,get,set" c:"姓名"`
+    	Age int `o:"find,get,set" c:"年纪"`
+    	School string `c:"学校"`
+    }
+
+  ```
+     结构体中不需要提供Id,CreateTime,UpdateTime三个字段,会自动生成   
+     结构体中每个tag有两个可选标签 c 和 o  
+     c是改字段的注释  
+     o是改字段需要生产的操作函数 可以取得get,find,set 分别表示生成返回单个对象的查询方法,返回多个对象的查询方法,设置该字段方法  
+     生成的目标文件会覆盖该简单go文件  
+     
+## goctl rpc生成
+
+  命令 `goctl rpc proto -proto ${proto} -service ${serviceName} -project ${projectName} -dir ${directory} -shared ${shared}`  
+  如: `goctl rpc proto -proto test.proto  -service test -project xjy  -dir .`  
+  
+  参数说明:
+  
+  - ${proto}: proto文件
+  - ${serviceName}: rpc服务名称
+  - ${projectName}: 所属项目,如xjy,xhb,crm,hera,具体查看help,主要为了根据不同项目服务往redis注册key,可选
+  - ${directory}: 输出目录
+  - ${shared}: shared文件生成目录,可选,默认为${pwd}/shared
+   
+  生成目录结构示例:
+  
+  ``` go
+	.
+    ├── shared [示例目录,可自己指定,强制覆盖更新]
+    │   └── contentservicemodel.go
+    ├── test
+    │   ├── etc
+    │   │   └── test.json
+    │   ├── internal
+    │   │   ├── config
+    │   │   │   └── config.go
+    │   │   ├── handler [强制覆盖更新]
+    │   │   │   ├── changeavatarhandler.go
+    │   │   │   ├── changebirthdayhandler.go
+    │   │   │   ├── changenamehandler.go
+    │   │   │   ├── changepasswordhandler.go
+    │   │   │   ├── changeuserinfohandler.go
+    │   │   │   ├── getuserinfohandler.go
+    │   │   │   ├── loginhandler.go
+    │   │   │   ├── logouthandler.go
+    │   │   │   └── testhandler.go
+    │   │   ├── logic
+    │   │   │   ├── changeavatarlogic.go
+    │   │   │   ├── changebirthdaylogic.go
+    │   │   │   ├── changenamelogic.go
+    │   │   │   ├── changepasswordlogic.go
+    │   │   │   ├── changeuserinfologic.go
+    │   │   │   ├── getuserinfologic.go
+    │   │   │   ├── loginlogic.go
+    │   │   │   └── logoutlogic.go
+    │   │   └── svc
+    │   │       └── servicecontext.go
+    │   ├── pb
+    │   │   └── test.pb.go
+    │   └── test.go [强制覆盖更新]
+    └── test.proto
+  ```
+  - 注意 :目前rpc目录生成的proto文件暂不支持import外部proto文件   
+* 如有不理解的地方,随时问Kim/Kevin

+ 126 - 0
tools/goctl/k8s/apirpc.go

@@ -0,0 +1,126 @@
+package k8s
+
+var apiRpcTmeplate = `apiVersion: apps/v1beta2
+kind: Deployment
+metadata:
+  name: {{.name}}
+  namespace: {{.namespace}}
+  labels:
+    app: {{.name}}
+spec:
+  replicas: {{.replicas}}
+  revisionHistoryLimit: {{.revisionHistoryLimit}}
+  selector:
+    matchLabels:
+      app: {{.name}}
+  template:
+    metadata:
+      labels:
+        app: {{.name}}
+    spec:{{if .envIsDev}}
+      terminationGracePeriodSeconds: 60{{end}}
+      containers:
+      - name: {{.name}}
+        image: registry-vpc.cn-hangzhou.aliyuncs.com/{{.namespace}}/
+        lifecycle:
+          preStop:
+            exec:
+              command: ["sh","-c","sleep 5"]
+        ports:
+        - containerPort: {{.port}}
+        readinessProbe:
+          tcpSocket:
+            port: {{.port}}
+          initialDelaySeconds: 5
+          periodSeconds: 10
+        livenessProbe:
+          tcpSocket:
+            port: {{.port}}
+          initialDelaySeconds: 15
+          periodSeconds: 20
+        env:
+        - name: aliyun_logs_k8slog
+          value: "stdout"
+        - name: aliyun_logs_k8slog_tags
+          value: "stage={{.env}}"
+        - name: aliyun_logs_k8slog_format
+          value: "json"
+        resources:
+          limits:
+            cpu: {{.limitCpu}}m
+            memory: {{.limitMem}}Mi
+          requests:
+            cpu: {{.requestCpu}}m
+            memory: {{.requestMem}}Mi
+        command:
+        - ./{{.serviceName}}
+        - -f
+        - ./{{.name}}.json
+        volumeMounts:
+        - name: timezone
+          mountPath: /etc/localtime
+      imagePullSecrets:
+      - name: {{.namespace}}
+      volumes:
+        - name: timezone
+          hostPath:
+            path: /usr/share/zoneinfo/Asia/Shanghai
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{.name}}-svc
+  namespace: {{.namespace}}
+spec:
+  ports:
+    - nodePort: 3{{.port}}
+      port: {{.port}}
+      protocol: TCP
+      targetPort: {{.port}}
+  selector:
+    app: {{.name}}
+  sessionAffinity: None
+  type: NodePort{{if .envIsPreOrPro}}
+
+---
+apiVersion: autoscaling/v2beta1
+kind: HorizontalPodAutoscaler
+metadata:
+  name: {{.name}}-hpa-c
+  namespace: {{.namespace}}
+  labels:
+    app: {{.name}}-hpa-c
+spec:
+  scaleTargetRef:
+    apiVersion: apps/v1beta1
+    kind: Deployment
+    name: di-api
+  minReplicas: {{.minReplicas}}
+  maxReplicas: {{.maxReplicas}}
+  metrics:
+  - type: Resource
+    resource:
+      name: cpu
+      targetAverageUtilization: 80
+
+---
+apiVersion: autoscaling/v2beta1
+kind: HorizontalPodAutoscaler
+metadata:
+  name: {{.name}}-hpa-m
+  namespace: {{.namespace}}
+  labels:
+    app: {{.name}}-hpa-m
+spec:
+  scaleTargetRef:
+    apiVersion: apps/v1beta1
+    kind: Deployment
+    name: {{.name}}
+  minReplicas: {{.minReplicas}}
+  maxReplicas: {{.maxReplicas}}
+  metrics:
+  - type: Resource
+    resource:
+      name: memory
+      targetAverageUtilization: 80{{end}}`

+ 46 - 0
tools/goctl/k8s/job.go

@@ -0,0 +1,46 @@
+package k8s
+
+// 无环境区分
+var jobTmeplate = `apiVersion: batch/v1beta1
+kind: CronJob
+metadata:
+  name: {{.name}}
+  namespace: {{.namespace}}
+spec:
+  successfulJobsHistoryLimit: {{.successfulJobsHistoryLimit}}
+  schedule: "{{.schedule}}"
+  jobTemplate:
+    spec:
+      template:
+        spec:
+          containers:
+          - name: {{.name}}
+            image: registry-vpc.cn-hangzhou.aliyuncs.com/{{.namespace}}/
+            env:
+            - name: aliyun_logs_k8slog
+              value: "stdout"
+            - name: aliyun_logs_k8slog_tags
+              value: "stage={{.env}}"
+            - name: aliyun_logs_k8slog_format
+              value: "json"
+            resources:
+              limits:
+                cpu: {{.limitCpu}}m
+                memory: {{.limitMem}}Mi
+              requests:
+                cpu: {{.requestCpu}}m
+                memory: {{.requestMem}}Mi
+            command:
+            - ./{{.serviceName}}
+            - -f
+            - ./{{.name}}.json
+            volumeMounts:
+            - name: timezone
+              mountPath: /etc/localtime
+          imagePullSecrets:
+          - name: {{.namespace}}
+          restartPolicy: OnFailure
+          volumes:
+          - name: timezone
+            hostPath:
+              path: /usr/share/zoneinfo/Asia/Shanghai`

+ 136 - 0
tools/goctl/k8s/k8s.go

@@ -0,0 +1,136 @@
+package k8s
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"text/template"
+)
+
+var (
+	errUnknownServiceType = errors.New("unknown service type")
+)
+
+const (
+	ServiceTypeApi  ServiceType = "api"
+	ServiceTypeRpc  ServiceType = "rpc"
+	ServiceTypeJob  ServiceType = "job"
+	ServiceTypeRmq  ServiceType = "rmq"
+	ServiceTypeSync ServiceType = "sync"
+	envDev                      = "dev"
+	envPre                      = "pre"
+	envPro                      = "pro"
+)
+
+type (
+	ServiceType string
+	K8sRequest  struct {
+		Env                        string
+		ServiceName                string
+		ServiceType                ServiceType
+		Namespace                  string
+		Schedule                   string
+		Replicas                   int
+		RevisionHistoryLimit       int
+		Port                       int
+		LimitCpu                   int
+		LimitMem                   int
+		RequestCpu                 int
+		RequestMem                 int
+		SuccessfulJobsHistoryLimit int
+		HpaMinReplicas             int
+		HpaMaxReplicas             int
+	}
+)
+
+func Gen(req K8sRequest) (string, error) {
+	switch req.ServiceType {
+	case ServiceTypeApi, ServiceTypeRpc:
+		return genApiRpc(req)
+	case ServiceTypeJob:
+		return genJob(req)
+	case ServiceTypeRmq, ServiceTypeSync:
+		return genRmqSync(req)
+	default:
+		return "", errUnknownServiceType
+	}
+}
+
+func genApiRpc(req K8sRequest) (string, error) {
+	t, err := template.New("api_rpc").Parse(apiRpcTmeplate)
+	if err != nil {
+		return "", err
+	}
+	buffer := new(bytes.Buffer)
+	err = t.Execute(buffer, map[string]interface{}{
+		"name":                 fmt.Sprintf("%s-%s", req.ServiceName, req.ServiceType),
+		"namespace":            req.Namespace,
+		"replicas":             req.Replicas,
+		"revisionHistoryLimit": req.RevisionHistoryLimit,
+		"port":                 req.Port,
+		"limitCpu":             req.LimitCpu,
+		"limitMem":             req.LimitMem,
+		"requestCpu":           req.RequestCpu,
+		"requestMem":           req.RequestMem,
+		"serviceName":          req.ServiceName,
+		"env":                  req.Env,
+		"envIsPreOrPro":        req.Env != envDev,
+		"envIsDev":             req.Env == envDev,
+		"minReplicas":          req.HpaMinReplicas,
+		"maxReplicas":          req.HpaMaxReplicas,
+	})
+	if err != nil {
+		return "", nil
+	}
+	return buffer.String(), nil
+}
+
+func genRmqSync(req K8sRequest) (string, error) {
+	t, err := template.New("rmq_sync").Parse(rmqSyncTmeplate)
+	if err != nil {
+		return "", err
+	}
+	buffer := new(bytes.Buffer)
+	err = t.Execute(buffer, map[string]interface{}{
+		"name":                 fmt.Sprintf("%s-%s", req.ServiceName, req.ServiceType),
+		"namespace":            req.Namespace,
+		"replicas":             req.Replicas,
+		"revisionHistoryLimit": req.RevisionHistoryLimit,
+		"limitCpu":             req.LimitCpu,
+		"limitMem":             req.LimitMem,
+		"requestCpu":           req.RequestCpu,
+		"requestMem":           req.RequestMem,
+		"serviceName":          req.ServiceName,
+		"env":                  req.Env,
+		"envIsPreOrPro":        req.Env != envDev,
+		"envIsDev":             req.Env == envDev,
+	})
+	if err != nil {
+		return "", nil
+	}
+	return buffer.String(), nil
+}
+
+func genJob(req K8sRequest) (string, error) {
+	t, err := template.New("job").Parse(jobTmeplate)
+	if err != nil {
+		return "", err
+	}
+	buffer := new(bytes.Buffer)
+	err = t.Execute(buffer, map[string]interface{}{
+		"name":                       fmt.Sprintf("%s-%s", req.ServiceName, req.ServiceType),
+		"namespace":                  req.Namespace,
+		"schedule":                   req.Schedule,
+		"successfulJobsHistoryLimit": req.SuccessfulJobsHistoryLimit,
+		"limitCpu":                   req.LimitCpu,
+		"limitMem":                   req.LimitMem,
+		"requestCpu":                 req.RequestCpu,
+		"requestMem":                 req.RequestMem,
+		"serviceName":                req.ServiceName,
+		"env":                        req.Env,
+	})
+	if err != nil {
+		return "", nil
+	}
+	return buffer.String(), nil
+}

+ 68 - 0
tools/goctl/k8s/rmqsync.go

@@ -0,0 +1,68 @@
+package k8s
+
+var rmqSyncTmeplate = `apiVersion: apps/v1beta2
+kind: Deployment
+metadata:
+  name: {{.name}}
+  namespace: {{.namespace}}
+  labels:
+    app: {{.name}}
+spec:
+  replicas: {{.replicas}}
+  revisionHistoryLimit: {{.revisionHistoryLimit}}
+  selector:
+    matchLabels:
+      app: {{.name}}
+  template:
+    metadata:
+      labels:
+        app: {{.name}}
+    spec:{{if .envIsDev}}
+      terminationGracePeriodSeconds: 60{{end}}
+      containers:
+      - name: {{.name}}
+        image: registry-vpc.cn-hangzhou.aliyuncs.com/{{.namespace}}/
+        lifecycle:
+          preStop:
+            exec:
+              command: ["sh","-c","sleep 5"]
+        env:
+        - name: aliyun_logs_k8slog
+          value: "stdout"
+        - name: aliyun_logs_k8slog_tags
+          value: "stage={{.env}}"
+        - name: aliyun_logs_k8slog_format
+          value: "json"
+        resources:
+          limits:
+            cpu: {{.limitCpu}}m
+            memory: {{.limitMem}}Mi
+          requests:
+            cpu: {{.requestCpu}}m
+            memory: {{.requestMem}}Mi
+        command:
+        - ./{{.serviceName}}
+        - -f
+        - ./{{.name}}.json
+        volumeMounts:
+        - name: timezone
+          mountPath: /etc/localtime
+      imagePullSecrets:
+      - name: {{.namespace}}
+      volumes:
+        - name: timezone
+          hostPath:
+            path: /usr/share/zoneinfo/Asia/Shanghai{{if .envIsPreOrPro}}
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{.name}}-svc
+  namespace: {{.namespace}}
+spec:
+  selector:
+    app: {{.name}}
+  sessionAffinity: None
+  type: ClusterIP
+  clusterIP: None{{end}}`

+ 33 - 0
tools/goctl/model/mongomodel/gen/genmethod.go

@@ -0,0 +1,33 @@
+package gen
+
+import (
+	"strings"
+
+	"zero/tools/goctl/model/mongomodel/utils"
+)
+
+func genMethodTemplate(funcDesc FunctionDesc, needCache bool) (template string) {
+	var tmp string
+	switch funcDesc.Type {
+	case functionTypeGet:
+		if needCache {
+			tmp = getTemplate
+		} else {
+			tmp = noCacheGetTemplate
+		}
+	case functionTypeFind:
+		tmp = findTemplate
+	case functionTypeSet:
+		if needCache {
+			tmp = ""
+		} else {
+			tmp = noCacheSetFieldtemplate
+		}
+	default:
+		return ""
+	}
+	tmp = strings.ReplaceAll(tmp, "{{.Name}}", funcDesc.FieldName)
+	tmp = strings.ReplaceAll(tmp, "{{.name}}", utils.UpperCamelToLower(funcDesc.FieldName))
+	tmp = strings.ReplaceAll(tmp, "{{.type}}", funcDesc.FieldType)
+	return tmp
+}

+ 156 - 0
tools/goctl/model/mongomodel/gen/genmongomodel.go

@@ -0,0 +1,156 @@
+package gen
+
+import (
+	"fmt"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/api/spec"
+	"zero/tools/goctl/api/util"
+	"zero/tools/goctl/model/mongomodel/utils"
+)
+
+const (
+	functionTypeGet  = "get"  //GetByField return single model
+	functionTypeFind = "find" // findByField return many model
+	functionTypeSet  = "set"  // SetField  only set specified field
+
+	TagOperate = "o" //字段函数的tag
+	TagComment = "c" //字段注释的tag
+)
+
+type (
+	FunctionDesc struct {
+		Type      string // get,find,set
+		FieldName string // 字段名字 eg:Age
+		FieldType string // 字段类型 eg: string,int64 等
+	}
+)
+
+func GenMongoModel(goFilePath string, needCache bool) error {
+	structs, imports, err := utils.ParseGoFile(goFilePath)
+	if err != nil {
+		return err
+	}
+	if len(structs) != 1 {
+		return fmt.Errorf("only 1 struct should be provided")
+	}
+	structStr, err := genStructs(structs)
+	if err != nil {
+		return err
+	}
+
+	fp, err := util.ClearAndOpenFile(goFilePath)
+	if err != nil {
+		return err
+	}
+	defer fp.Close()
+
+	var myTemplate string
+	if needCache {
+		myTemplate = cacheTemplate
+	} else {
+		myTemplate = noCacheTemplate
+	}
+	structName := getStructName(structs)
+	functionList := getFunctionList(structs)
+
+	for _, fun := range functionList {
+		funTmp := genMethodTemplate(fun, needCache)
+		if funTmp == "" {
+			continue
+		}
+		myTemplate += "\n"
+		myTemplate += funTmp
+		myTemplate += "\n"
+	}
+
+	t := template.Must(template.New("mongoTemplate").Parse(myTemplate))
+	return t.Execute(fp, map[string]string{
+		"modelName":   structName,
+		"importArray": getImports(imports, needCache),
+		"modelFields": structStr,
+	})
+}
+
+func getFunctionList(structs []utils.Struct) []FunctionDesc {
+	var list []FunctionDesc
+	for _, field := range structs[0].Fields {
+		tagMap := parseTag(field.Tag)
+		if fun, ok := tagMap[TagOperate]; ok {
+			funList := strings.Split(fun, ",")
+			for _, o := range funList {
+				var f FunctionDesc
+				f.FieldType = field.Type
+				f.FieldName = field.Name
+				f.Type = o
+				list = append(list, f)
+			}
+		}
+	}
+	return list
+}
+
+func getStructName(structs []utils.Struct) string {
+	for _, structItem := range structs {
+		return structItem.Name
+	}
+	return ""
+}
+
+func genStructs(structs []utils.Struct) (string, error) {
+	if len(structs) > 1 {
+		return "", fmt.Errorf("input .go file must only one struct")
+	}
+
+	modelFields := `Id             bson.ObjectId ` + quotationMark + `bson:"_id" json:"id,omitempty"` + quotationMark + "\n\t"
+	for _, structItem := range structs {
+		for _, field := range structItem.Fields {
+			modelFields += getFieldLine(field)
+		}
+	}
+	modelFields += "\t" + `CreateTime time.Time ` + quotationMark + `json:"createTime,omitempty" bson:"createTime"` + quotationMark + "\n\t"
+	modelFields += "\t" + `UpdateTime time.Time ` + quotationMark + `json:"updateTime,omitempty" bson:"updateTime"` + quotationMark
+	return modelFields, nil
+}
+
+func getFieldLine(member spec.Member) string {
+	if member.Name == "CreateTime" || member.Name == "UpdateTime" || member.Name == "Id" {
+		return ""
+	}
+	jsonName := utils.UpperCamelToLower(member.Name)
+	result := "\t" + member.Name + ` ` + member.Type + ` ` + quotationMark + `json:"` + jsonName + `,omitempty"` + ` bson:"` + jsonName + `"` + quotationMark
+	tagMap := parseTag(member.Tag)
+	if comment, ok := tagMap[TagComment]; ok {
+		result += ` //` + comment + "\n\t"
+	} else {
+		result += "\n\t"
+	}
+	return result
+}
+
+// tag like `o:"find,get,update" c:"姓名"`
+func parseTag(tag string) map[string]string {
+	var result = make(map[string]string, 0)
+	tags := strings.Split(tag, " ")
+	for _, kv := range tags {
+		temp := strings.Split(kv, ":")
+		if len(temp) > 1 {
+			key := strings.ReplaceAll(strings.ReplaceAll(temp[0], "\"", ""), "`", "")
+			value := strings.ReplaceAll(strings.ReplaceAll(temp[1], "\"", ""), "`", "")
+			result[key] = value
+		}
+	}
+	return result
+}
+
+func getImports(imports []string, needCache bool) string {
+
+	importStr := strings.Join(imports, "\n\t")
+	importStr += "\"errors\"\n\t"
+	importStr += "\"time\"\n\t"
+	importStr += "\n\t\"zero/core/stores/cache\"\n\t"
+	importStr += "\"zero/core/stores/mongoc\"\n\t"
+	importStr += "\n\t\"github.com/globalsign/mgo/bson\""
+	return importStr
+}

+ 67 - 0
tools/goctl/model/mongomodel/gen/genmongomodelbynetwork.go

@@ -0,0 +1,67 @@
+package gen
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"strings"
+	"text/template"
+
+	"zero/tools/goctl/model/mongomodel/utils"
+)
+
+func GenMongoModelByNetwork(input string, needCache bool) (string, error) {
+	if strings.TrimSpace(input) == "" {
+		return "", errors.New("struct不能为空")
+	}
+	if strings.Index(strings.TrimSpace(input), "type") != 0 {
+		input = "type " + input
+	}
+
+	if strings.Index(strings.TrimSpace(input), "package") != 0 {
+		input = "package model\r\n" + input
+	}
+
+	structs, imports, err := utils.ParseGoFileByNetwork(input)
+	if err != nil {
+		return "", err
+	}
+	if len(structs) != 1 {
+		return "", fmt.Errorf("only 1 struct should be provided")
+	}
+	structStr, err := genStructs(structs)
+	if err != nil {
+		return "", err
+	}
+
+	var myTemplate string
+	if needCache {
+		myTemplate = cacheTemplate
+	} else {
+		myTemplate = noCacheTemplate
+	}
+	structName := getStructName(structs)
+	functionList := getFunctionList(structs)
+
+	for _, fun := range functionList {
+		funTmp := genMethodTemplate(fun, needCache)
+		if funTmp == "" {
+			continue
+		}
+		myTemplate += "\n"
+		myTemplate += funTmp
+		myTemplate += "\n"
+	}
+
+	t := template.Must(template.New("mongoTemplate").Parse(myTemplate))
+	var result bytes.Buffer
+	err = t.Execute(&result, map[string]string{
+		"modelName":   structName,
+		"importArray": getImports(imports, needCache),
+		"modelFields": structStr,
+	})
+	if err != nil {
+		return "", err
+	}
+	return result.String(), nil
+}

+ 238 - 0
tools/goctl/model/mongomodel/gen/templatemodel.go

@@ -0,0 +1,238 @@
+package gen
+
+const (
+	quotationMark = "`"
+	//templates that do not use caching
+	noCacheTemplate = `package model
+
+import (
+	{{.importArray}}
+)
+
+var ErrNotFound = mongoc.ErrNotFound
+
+type (
+	{{.modelName}}Model struct {
+		*mongoc.Model
+	}
+
+	{{.modelName}} struct {
+		{{.modelFields}}
+	}
+)
+
+func New{{.modelName}}Model(url, database, collection string, c cache.CacheConf, opts ...cache.Option) *{{.modelName}}Model {
+	return &{{.modelName}}Model{mongoc.MustNewModel(url, database, collection, c, opts...)}
+}
+
+func (m *{{.modelName}}Model) FindOne(id string) (*{{.modelName}}, error) {
+	session, err := m.Model.TakeSession()
+	if err != nil {
+		return nil, err
+	}
+	defer m.Model.PutSession(session)
+
+	var result {{.modelName}}
+	err = m.GetCollection(session).FindOneIdNoCache(&result,bson.ObjectIdHex(id))
+	if err != nil {
+		return nil, err
+	}
+	return &result, nil
+}
+
+func (m *{{.modelName}}Model) Delete(id string) error {
+	session, err := m.TakeSession()
+	if err != nil {
+		return err
+	}
+	defer m.PutSession(session)
+	return m.GetCollection(session).RemoveIdNoCache(bson.ObjectIdHex(id))
+}
+
+func (m *{{.modelName}}Model) Insert(data *{{.modelName}}) error {
+	session, err := m.TakeSession()
+	if err != nil {
+		return err
+	}
+	defer m.PutSession(session)
+
+	return m.GetCollection(session).Insert(data)
+}
+
+func (m *{{.modelName}}Model) Update(data *{{.modelName}}) error {
+	session, err := m.TakeSession()
+	if err != nil {
+		return err
+	}
+	defer m.PutSession(session)
+
+	data.UpdateTime = time.Now()
+	return m.GetCollection(session).UpdateIdNoCache(data.Id, data)
+}
+`
+
+	//use cache template
+	cacheTemplate = `package model
+
+import (
+	{{.importArray}}
+)
+
+var ErrNotFound = errors.New("not found")
+
+const (
+	Prefix{{.modelName}}CacheKey = "#{{.modelName}}#cache" //todo please modify this prefix
+)
+
+type (
+	{{.modelName}}Model struct {
+		*mongoc.Model
+	}
+
+	{{.modelName}} struct {
+		{{.modelFields}}
+	}
+)
+
+func New{{.modelName}}Model(url, database, collection string, c cache.CacheConf, opts ...cache.Option) *{{.modelName}}Model {
+	return &{{.modelName}}Model{mongoc.MustNewModel(url, database, collection, c, opts...)}
+}
+
+func (m *{{.modelName}}Model) FindOne(id string) (*{{.modelName}}, error) {
+	key := Prefix{{.modelName}}CacheKey + id
+	session, err := m.Model.TakeSession()
+	if err != nil {
+		return nil, err
+	}
+	defer m.Model.PutSession(session)
+
+	var result {{.modelName}}
+	err = m.GetCollection(session).FindOneId(&result, key, bson.ObjectIdHex(id))
+	switch err {
+	case nil:
+		return &result, nil
+	case mongoc.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}
+
+func (m *{{.modelName}}Model) Delete(id string) error {
+	session, err := m.TakeSession()
+	if err != nil {
+		return err
+	}
+	defer m.PutSession(session)
+
+	key := Prefix{{.modelName}}CacheKey + id
+	return m.GetCollection(session).RemoveId(bson.ObjectIdHex(id), key)
+}
+
+func (m *{{.modelName}}Model) Insert(data *{{.modelName}}) error {
+	session, err := m.TakeSession()
+	if err != nil {
+		return err
+	}
+	defer m.PutSession(session)
+
+	return m.GetCollection(session).Insert(data)
+}
+
+func (m *{{.modelName}}Model) Update(data *{{.modelName}}) error {
+	session, err := m.TakeSession()
+	if err != nil {
+		return err
+	}
+	defer m.PutSession(session)
+
+	data.UpdateTime = time.Now()
+	key := Prefix{{.modelName}}CacheKey + data.Id.Hex()
+	return m.GetCollection(session).UpdateId(data.Id, data, key)
+}
+`
+	cacheSetFieldtemplate = `func (m *{{.modelName}}Model) Set{{.Name}}(id string, {{.name}} {{.type}}) error {
+	_, err := m.cache.Del(Prefix{{.modelName}}CacheKey + id)
+	if err != nil {
+		return err
+	}
+
+	session, err := m.TakeSession()
+	if err != nil {
+		return err
+	}
+	defer m.PutSession(session)
+
+	update := bson.M{"$set": bson.M{"{{.name}}": {{.name}}, "updateTime": time.Now()}}
+	return m.GetCollection(session).UpdateId(bson.ObjectIdHex(id), update)
+}`
+
+	noCacheSetFieldtemplate = `func (m *{{.modelName}}Model) Set{{.Name}}(id string, {{.name}} {{.type}}) error {
+	session, err := m.TakeSession()
+	if err != nil {
+		return err
+	}
+	defer m.PutSession(session)
+
+	update := bson.M{"$set": bson.M{"{{.name}}": {{.name}}, "updateTime": time.Now()}}
+	return m.GetCollection(session).UpdateId(bson.ObjectIdHex(id), update)
+}`
+
+	noCacheGetTemplate = `func (m *{{.modelName}}Model) GetBy{{.Name}}({{.name}} {{.type}}) (*{{.modelName}},error) {
+	session, err := m.TakeSession()
+	if err != nil {
+		return nil,err
+	}
+	defer m.PutSession(session)
+	var result {{.modelName}}
+	query := bson.M{"{{.name}}":{{.name}}}
+	err = m.GetCollection(session).FindOneNoCache(&result, query)
+	if err != nil {
+		if err == mgo.ErrNotFound {
+			return nil,ErrNotFound
+		}
+		return nil,err
+	}
+	return &result,nil
+}`
+	// GetByField return single model
+	getTemplate = `func (m *{{.modelName}}Model) GetBy{{.Name}}({{.name}} {{.type}}) (*{{.modelName}},error) {
+	session, err := m.TakeSession()
+	if err != nil {
+		return nil,err
+	}
+	defer m.PutSession(session)
+	var result {{.modelName}}
+	query := bson.M{"{{.name}}":{{.name}}}
+	key := getCachePrimaryKeyBy{{.Name}}({{.name}})
+	err = m.GetCollection(session).FindOne(&result,key,query)
+	if err != nil {
+		if err == mgo.ErrNotFound {
+			return nil,ErrNotFound
+		}
+		return nil,err
+	}
+	return &result,nil
+}
+
+func getCachePrimaryKeyBy{{.Name}}({{.name}} {{.type}}) string {
+	return "" //todo 请补全这里
+}
+`
+
+	findTemplate = `func (m *{{.modelName}}Model) FindBy{{.Name}}({{.name}} string) ([]{{.modelName}},error) {
+	session, err := m.TakeSession()
+	if err != nil {
+		return nil,err
+	}
+	defer m.PutSession(session)
+	
+	query := bson.M{"{{.name}}":{{.name}}}
+	var result []{{.modelName}}
+	err = m.GetCollection(session).FindAllNoCache(&result,query)
+	if err != nil {
+		return nil,err
+	}
+	return result,nil
+}`
+)

+ 30 - 0
tools/goctl/model/mongomodel/genmongocmd.go

@@ -0,0 +1,30 @@
+package mongomodel
+
+import (
+	"errors"
+	"fmt"
+
+	"zero/core/lang"
+	"zero/tools/goctl/model/mongomodel/gen"
+
+	"github.com/logrusorgru/aurora"
+	"github.com/urfave/cli"
+)
+
+func ModelCommond(c *cli.Context) error {
+	src := c.String("src")
+	cache := c.String("cache")
+
+	if len(src) == 0 {
+		return errors.New("missing -src")
+	}
+	var needCache bool
+	if cache == "yes" {
+		needCache = true
+	}
+
+	lang.Must(gen.GenMongoModel(src, needCache))
+
+	fmt.Println(aurora.Green("Done."))
+	return nil
+}

+ 167 - 0
tools/goctl/model/mongomodel/utils/parsesimplegofile.go

@@ -0,0 +1,167 @@
+package utils
+
+import (
+	"fmt"
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"io/ioutil"
+	"strings"
+
+	"zero/tools/goctl/api/spec"
+)
+
+const (
+	StructArr = "struct"
+	ImportArr = "import"
+	Unknown   = "unknown"
+)
+
+type Struct struct {
+	Name   string
+	Fields []spec.Member
+}
+
+func readFile(filePath string) (string, error) {
+	b, err := ioutil.ReadFile(filePath)
+	if err != nil {
+		return "", err
+	}
+	return string(b), nil
+}
+
+func ParseNetworkGoFile(io string) ([]Struct, []string, error) {
+	fset := token.NewFileSet() // 位置是相对于节点
+
+	f, err := parser.ParseFile(fset, "", io, 0)
+	if err != nil {
+		return nil, nil, err
+	}
+	return parse(f)
+}
+
+func ParseGoFile(pathOrStr string) ([]Struct, []string, error) {
+	var goFileStr string
+	var err error
+	goFileStr, err = readFile(pathOrStr)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	fset := token.NewFileSet() // 位置是相对于节点
+
+	f, err := parser.ParseFile(fset, "", goFileStr, 0)
+	if err != nil {
+		return nil, nil, err
+	}
+	return parse(f)
+}
+
+func ParseGoFileByNetwork(io string) ([]Struct, []string, error) {
+	fset := token.NewFileSet() // 位置是相对于节点
+
+	f, err := parser.ParseFile(fset, "", io, 0)
+	if err != nil {
+		return nil, nil, err
+	}
+	return parse(f)
+}
+
+//使用ast包解析golang文件
+func parse(f *ast.File) ([]Struct, []string, error) {
+	if len(f.Decls) == 0 {
+		return nil, nil, fmt.Errorf("you should provide as least 1 struct")
+	}
+	var structList []Struct
+	var importList []string
+	for _, decl := range f.Decls {
+		structs, imports, err := getStructAndImportInfo(decl)
+		if err != nil {
+			return nil, nil, err
+		}
+		structList = append(structList, structs...)
+		importList = append(importList, imports...)
+	}
+	return structList, importList, nil
+}
+
+func getStructAndImportInfo(decl ast.Decl) (structs []Struct, imports []string, err error) {
+	var structList []Struct
+	var importList []string
+	genDecl, ok := decl.(*ast.GenDecl)
+	if !ok {
+		return nil, nil, fmt.Errorf("please input right file")
+	}
+	for _, tpyObj := range genDecl.Specs {
+		switch tpyObj.(type) {
+		case *ast.ImportSpec: // import
+			importSpec := tpyObj.(*ast.ImportSpec)
+			s := importSpec.Path.Value
+			importList = append(importList, s)
+		case *ast.TypeSpec: //type
+			typeSpec := tpyObj.(*ast.TypeSpec)
+			switch typeSpec.Type.(type) {
+			case *ast.StructType: // struct
+				struct1, err := parseStruct(typeSpec)
+				if err != nil {
+					return nil, nil, err
+				}
+				structList = append(structList, *struct1)
+			}
+		default:
+
+		}
+	}
+	return structList, importList, nil
+}
+
+func parseStruct(typeSpec *ast.TypeSpec) (*Struct, error) {
+	var result Struct
+	structName := typeSpec.Name.Name
+	result.Name = structName
+	structType, ok := typeSpec.Type.(*ast.StructType)
+	if !ok {
+		return nil, fmt.Errorf("not struct")
+	}
+
+	for _, item := range structType.Fields.List {
+		var member spec.Member
+		var err error
+		member.Name = parseFiledName(item.Names)
+		member.Type, err = parseFiledType(item.Type)
+		if err != nil {
+			return nil, err
+		}
+		if item.Tag != nil {
+			member.Tag = item.Tag.Value
+		}
+		result.Fields = append(result.Fields, member)
+	}
+	return &result, nil
+}
+
+func parseFiledType(expr ast.Expr) (string, error) {
+	switch expr.(type) {
+	case *ast.Ident:
+		return expr.(*ast.Ident).Name, nil
+	case *ast.SelectorExpr:
+		selectorExpr := expr.(*ast.SelectorExpr)
+		return selectorExpr.X.(*ast.Ident).Name + "." + selectorExpr.Sel.Name, nil
+	default:
+		return "", fmt.Errorf("can't parse type")
+	}
+}
+
+func parseFiledName(idents []*ast.Ident) string {
+	for _, name := range idents {
+		return name.Name
+	}
+	return ""
+}
+
+func UpperCamelToLower(name string) string {
+	if len(name) == 0 {
+		return ""
+	}
+	return strings.ToLower(name[:1]) + name[1:]
+}

+ 430 - 0
tools/goctl/model/sql/README.MD

@@ -0,0 +1,430 @@
+<div style="text-align: center;"><h1>Sql生成工具说明文档</h1></div>
+
+<h2>前言</h2>
+在当前Sql代码生成工具是基于sqlc生成的逻辑。
+
+<h2>关键字</h2>
+
++ 查询类型(前暂不支持同一字段多种类型混合生成,如按照campus_id查询单结果又查询All或者Limit)
+  - 单结果查询
+    - FindOne(主键特有)
+    - FindOneByXxx
+  - 多结果查询
+    - FindAllByXxx
+    - FindLimitByXxx
+- withCache
+- withoutCache
+
+<h2>准备工作</h2>
+
+- table
+
+    ```
+    CREATE TABLE `user_info` (
+    `id` bigint(20) NOT NULL COMMENT '主键',
+    `campus_id` bigint(20) DEFAULT NULL COMMENT '整校id',
+    `name` varchar(255) DEFAULT NULL COMMENT '用户姓名',
+    `id_number` varchar(255) DEFAULT NULL COMMENT '身份证',
+    `age` int(10) DEFAULT NULL COMMENT '年龄',
+    `gender` tinyint(1) DEFAULT NULL COMMENT '性别,0-男,1-女,2-不限',
+    `mobile` varchar(20) DEFAULT NULL COMMENT '手机号',
+    `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`)
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+    ```
+
+<h2>imports生成</h2>
+imports代码生成对应model中包的引入管理,仅使用于晓黑板项目中(非相对路径动态生成),目前受`withCache`参数的影响,除此之外其实为固定代码。
+
+- withCache
+  
+    ```
+    import (
+        "database/sql""fmt"
+        "strings"
+        "time"
+
+        "zero/core/stores/sqlc"
+        "zero/core/stores/sqlx"
+        "zero/core/stringx"
+        "xiao/service/shared/builderx"
+    )
+    ```
+
+- withoutCache
+  
+    ```
+    import (
+        "database/sql""fmt"
+        "strings"
+        "time"
+
+        "zero/core/stores/sqlx"
+        "zero/core/stringx"
+        "xiao/service/shared/builderx"
+    )
+    ```
+
+<h2>vars生成</h2>
+
+vars部分对应model中var声明的包含的代码块,由`table`名和`withCache`来决定其中的代码生成内容,`withCache`决定是否要生成缓存key变量的声明。
+
+- withCache
+
+    ```
+    var (
+        UserInfoFieldNames          = builderx.FieldNames(&UserInfo{})
+        UserInfoRows                = strings.Join(UserInfoFieldNames, ",")
+        UserInfoRowsExpectAutoSet   = strings.Join(stringx.Remove(UserInfoFieldNames, "id", "create_time", "update_time"), ",")
+        UserInfoRowsWithPlaceHolder = strings.Join(stringx.Remove(UserInfoFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
+    
+        cacheUserInfoIdPrefix = "cache#userInfo#id#"
+        cacheUserInfoCampusIdPrefix = "cache#userInfo#campusId#"
+        cacheUserInfoNamePrefix = "cache#userInfo#name#"
+        cacheUserInfoMobilePrefix = "cache#userInfo#mobile#"
+    )
+    ```
+
+- withoutCache
+
+    ```
+    var (
+        UserInfoFieldNames          = builderx.FieldNames(&UserInfo{})
+        UserInfoRows                = strings.Join(UserInfoFieldNames, ",")
+        UserInfoRowsExpectAutoSet   = strings.Join(stringx.Remove(UserInfoFieldNames, "id", "create_time", "update_time"), ",")
+        UserInfoRowsWithPlaceHolder = strings.Join(stringx.Remove(UserInfoFieldNames, "id", "create_time", "update_time"), "=?,") + "=?"
+    )
+    ```
+
+<h2>types生成</h2>
+
+ypes部分对应model中type声明的包含的代码块,由`table`名和`withCache`来决定其中的代码生成内容,`withCache`决定引入sqlc还是sqlx。
+
+- withCache
+    ```
+    type (
+        UserInfoModel struct {
+            conn  sqlc.CachedConn
+            table string
+        }
+
+        UserInfo struct {
+            Id int64 `db:"id"` // 主键id
+            CampusId int64 `db:"campus_id"` // 整校id
+            Name string `db:"name"` // 用户姓名
+            IdNumber string `db:"id_number"` // 身份证
+            Age int64 `db:"age"` // 年龄
+            Gender int64 `db:"gender"` // 性别,0-男,1-女,2-不限
+            Mobile string `db:"mobile"` // 手机号
+            CreateTime time.Time `db:"create_time"` // 创建时间
+            UpdateTime time.Time `db:"update_time"` // 更新时间
+        }
+    )    
+    ```
+
+- withoutCache
+    ```
+    type (
+        UserInfoModel struct {
+            conn  sqlx.SqlConn
+            table string
+        }
+
+        UserInfo struct {
+            Id int64 `db:"id"` // 主键id
+            CampusId int64 `db:"campus_id"` // 整校id
+            Name string `db:"name"` // 用户姓名
+            IdNumber string `db:"id_number"` // 身份证
+            Age int64 `db:"age"` // 年龄
+            Gender int64 `db:"gender"` // 性别,0-男,1-女,2-不限
+            Mobile string `db:"mobile"` // 手机号
+            CreateTime time.Time `db:"create_time"` // 创建时间
+            UpdateTime time.Time `db:"update_time"` // 更新时间
+        }
+    )
+    ```
+<h2>New生成</h2>
+new生成对应model中struct的New函数,受`withCache`影响决定是否要引入cacheRedis
+
+- withCache
+    ```
+    func NewUserInfoModel(conn sqlx.SqlConn, c cache.CacheConf, table string) *UserInfoModel {
+        return &UserInfoModel{
+            CachedConn: sqlc.NewConn(conn, c),
+            table:      table,
+        }
+    }
+    ```
+- withoutCache
+    ```
+    func NewUserInfoModel(conn sqlx.SqlConn, table string) *UserInfoModel {
+	    return &UserInfoModel{conn: conn, table: table}
+    }
+    ```
+
+
+<h2>FindOne查询生成</h2>
+FindOne查询代码生成仅对主键有效。如`user_info`中生成的FindOne如下:
+
+- withCache
+
+    ```
+    func (m *UserInfoModel) FindOne(id int64) (*UserInfo, error) {
+        idKey := fmt.Sprintf("%s%v", cacheUserInfoIdPrefix, id)
+        var resp UserInfo
+        err := m.QueryRow(&resp, idKey, func(conn sqlx.SqlConn, v interface{}) error {
+            query := `select ` + userInfoRows + ` from ` + m.table  + `where id = ? limit 1`
+            return conn.QueryRow(v, query, id)
+        })
+        switch err {
+        case nil:
+            return &resp, nil
+        case sqlc.ErrNotFound:
+            return nil, ErrNotFound
+        default:
+            return nil, err
+        }
+    }
+    ```
+
+- withoutCache
+
+    ```
+    func (m *UserInfoModel) FindOne(id int64) (*UserInfo, error) {
+        
+        query := `select ` + userInfoRows + ` from ` + m.table  + `where id = ? limit 1`
+        var resp UserInfo
+        err := m.conn.QueryRow(&resp, query, id)
+        switch err {
+        case nil:
+            return &resp, nil
+        case sqlx.ErrNotFound:
+            return nil, ErrNotFound
+        default:
+            return nil, err
+        
+    }
+    ```
+
+<h2>FindOneByXxx查询生成</h2>
+
+FindOneByXxx查询生成可以按照单个字段查询、多个字段以AND关系且表达式符号为`=`的查询(下称:组合查询),对除主键之外的字段有效,对于单个字段可以用`withCache`来控制是否需要缓存,这里的缓存只缓存主键,并不缓存整个struct,注意:这里有一个隐藏的规则,如果单个字段查询需要cache,那么主键一定有cache;多个字段组成的`组合查询`一律没有缓存处理,<strong><i>且组合查询不能相互嵌套</i></strong>,否则会报`circle query with other fields`错误,下面我们按场景来依次查看对应代码生成后的示例。
+
+>注:目前暂不支持除equals之外的条件查询。
+
++ 单字段查询  
+    以name查询为例
+    - withCache
+        ```
+        func (m *UserInfoModel) FindOneByName(name string) (*UserInfo, error) {
+            nameKey := fmt.Sprintf("%s%v", cacheUserInfoNamePrefix, name)
+            var id string
+            err := m.GetCache(key, &id)
+            if err != nil {
+                return nil, err
+            }
+            if id != "" {
+                return m.FindOne(id)
+            }
+            var resp UserInfo
+            query := `select ` + userInfoRows + ` from ` + m.table  + `where name = ? limit 1`
+            err = m.QueryRowNoCache(&resp, query, name)
+            switch err {
+            case nil:
+                err = m.SetCache(nameKey, resp.Id)
+                if err != nil {
+                    logx.Error(err)
+                }
+                return &resp, nil
+            case sqlc.ErrNotFound:
+                return nil, ErrNotFound
+            default:
+                return nil, err
+            }
+        }    
+        ```
+    - withoutCache
+
+        ```
+        func (m *UserInfoModel) FindOneByName(name string) (*UserInfo, error) {
+            var resp UserInfo
+            query := `select ` + userInfoRows + ` from ` + m.table  + `where name = ? limit 1`
+            err = m.conn.QueryRow(&resp, query, name)
+            switch err {
+            case nil:
+                return &resp, nil
+            case sqlx.ErrNotFound:
+                return nil, ErrNotFound
+            default:
+                return nil, err
+            }
+        }
+        ```
+
+- 组合查询  
+    以`campus_id`和`id_number`查询为例。
+    
+        ```
+        func (m *UserInfoModel) FindOneByCampusIdAndIdNumber(campusId int64,idNumber string) (*UserInfo, error) {
+            var resp UserInfo
+            query := `select ` + userInfoRows + ` from ` + m.table  + `where campus_id = ? AND id_number = ? limit 1`
+            err = m.QueryRowNoCache(&resp, query, campusId, idNumber)
+            // err = m.conn.QueryRows(&resp, query, campusId, idNumber)
+            switch err {
+            case nil:
+                return &resp, nil
+            case sqlx.ErrNotFound:
+                return nil, ErrNotFound
+            default:
+                return nil, err
+            }
+        }
+        ```
+<h2>FindAllByXxx生成</h2>
+FindAllByXxx查询和FindOneByXxx功能相似,只是FindOneByXxx限制了limit等于1,而FindAllByXxx是查询所有,以两个例子来说明
+
+- 查询单个字段`name`等于某值的所有数据
+    ```
+    func (m *UserInfoModel) FindAllByName(name string) ([]*UserInfo, error) {
+        var resp []*UserInfo
+        query := `select ` + userInfoRows + ` from ` + m.table  + `where name = ?`
+        err := m.QueryRowsNoCache(&resp, query, name)
+        // err := m.conn.QueryRows(&resp, query, name)
+        if err != nil {
+            return nil, err
+        }
+        return resp, nil
+    }    
+    ```
+- 查询多个组合字段`campus_id`等于某值且`gender`等于某值的所有数据
+    ```
+    func (m *UserInfoModel) FindAllByCampusIdAndGender(campusId int64,gender int64) ([]*UserInfo, error) {
+        var resp []*UserInfo
+        query := `select ` + userInfoRows + ` from ` + m.table  + `where campus_id = ? AND gender = ?`
+        err := m.QueryRowsNoCache(&resp, query, campusId, gender)
+        // err := m.conn.QueryRows(&resp, query, campusId, gender)
+        if err != nil {
+            return nil, err
+        }
+        return resp, nil
+    }    
+    ```
+
+<h2>FindLimitByXxx生成</h2>
+FindLimitByXxx查询和FindAllByXxx功能相似,只是FindAllByXxx限制了limit,除此之外还会生成查询对应Count总数的代码,而FindAllByXxx是查询所有数据,以几个例子来说明
+
+- 查询`gender`等于某值的分页数据,按照`create_time`降序
+    ```
+    func (m *UserInfoModel) FindLimitByGender(gender int64, page, limit int) ([]*UserInfo, error) {
+        var resp []*UserInfo
+        query := `select ` + userInfoRows + `from ` + m.table  + `where gender = ? order by create_time DESC limit ?,?`
+        err := m.QueryRowsNoCache(&resp, query, gender, (page-1)*limit, limit)
+        // err := m.conn.QueryRows(&resp, query, gender, (page-1)*limit, limit)
+        if err != nil {
+            return nil, err
+        }
+        return resp, nil
+    }
+
+    func (m *UserInfoModel) FindAllCountByGender(gender int64) (int64, error) {
+        var count int64
+        query := `select count(1)  from ` + m.table  + `where gender = ? `
+        err := m.QueryRowsNoCache(&count, query, gender)
+        // err := m.conn.QueryRow(&count, query, gender)
+        if err != nil {
+            return 0, err
+        }
+        return count, nil
+    }       
+    ```
+- 查询`gender`等于某值的分页数据,按照`create_time`降序、`update_time`生序排序
+    ```
+    func (m *UserInfoModel) FindLimitByGender(gender int64, page, limit int) ([]*UserInfo, error) {
+        var resp []*UserInfo
+        query := `select ` + userInfoRows + `from ` + m.table  + `where gender = ? order by create_time DESC,update_time ASC limit ?,?`
+        err := m.QueryRowsNoCache(&resp, query, gender, (page-1)*limit, limit)
+       // err := m.conn.QueryRows(&resp, query, gender, (page-1)*limit, limit)
+        if err != nil {
+            return nil, err
+        }
+        return resp, nil
+    }
+
+    func (m *UserInfoModel) FindAllCountByGender(gender int64) (int64, error) {
+        var count int64
+        query := `select count(1)  from ` + m.table  + `where gender = ? `
+        err := m.QueryRowNoCache(&count, query, gender)
+        // err := m.conn.QueryRow(&count, query, gender)
+        if err != nil {
+            return 0, err
+        }
+        return count, nil
+    }          
+    ```
+- 查询`gender`等于某值且`campus_id`为某值按照`create_time`降序的分页数据
+    ```
+    func (m *UserInfoModel) FindLimitByGenderAndCampusId(gender int64,campusId int64, page, limit int) ([]*UserInfo, error) {
+        var resp []*UserInfo
+        query := `select ` + userInfoRows + `from ` + m.table  + `where gender = ? AND campus_id = ? order by create_time DESC limit ?,?`
+        err := m.QueryRowsNoCache(&resp, query, gender, campusId, (page-1)*limit, limit)
+        // err := m.conn.QueryRows(&resp, query, gender, campusId, (page-1)*limit, limit)
+        if err != nil {
+            return nil, err
+        }
+        return resp, nil
+    }
+
+    func (m *UserInfoModel) FindAllCountByGenderAndCampusId(gender int64,campusId int64) (int64, error) {
+        var count int64
+        query := `select count(1)  from ` + m.table  + `where gender = ? AND campus_id = ? `
+        err := m.QueryRowsNoCache(&count, query, gender, campusId)
+        // err := m.conn.QueryRow(&count, query, gender, campusId)
+        if err != nil {
+            return 0, err
+        }
+        return count, nil
+    }      
+    ```
+
+<h2>Delete生成</h2>
+Delete代码根据`withCache`的不同可以生成带缓存逻辑代码和不带缓存逻辑代码,<strong><i>Delete代码生成仅按照主键删除</i></strong>。从FindOneByXxx方法描述得知,非主键`withCache`了那么主键会强制被cache,因此在delete时也会删除主键cache。
+
+- withCache  
+  根据`mobile`查询用户信息
+
+    ```
+    func (m *UserInfoModel) Delete(userId int64) error {
+        userIdKey := fmt.Sprintf("%s%v", cacheUserInfoUserIdPrefix, userId)
+        mobileKey := fmt.Sprintf("%s%v", cacheUserInfoMobilePrefix, mobile)
+        _, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
+            query := `delete from ` + m.table +  + `where user_id = ?`
+            return conn.Exec(query, userId)
+        }, userIdKey, mobileKey)
+        return err
+        }
+    ```
+- withoutCache
+    ```
+    func (m *UserInfoModel) Delete(userId int64) error {
+	_, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) {
+		query := `delete from ` + m.table +  + `where user_id = ?`
+		return conn.Exec(query, userId)
+	}, )
+	return err
+}
+    ```
+<h2>Insert生成</h2>
+
+<h2>Update生成</h2>
+
+<h2>待完善(TODO)</h2>
+
+- 同一字段多种查询方式代码生成(优先级较高)
+- 条件查询
+- 范围查询
+- ...
+
+<h2>反馈与建议</h2>
+
+- 无

+ 108 - 0
tools/goctl/model/sql/gen/convert.go

@@ -0,0 +1,108 @@
+package gen
+
+import (
+	"errors"
+	"fmt"
+	"sort"
+	"strings"
+
+	"zero/tools/goctl/model/sql/util"
+)
+
+func TableConvert(outerTable OuterTable) (*InnerTable, error) {
+	var table InnerTable
+	table.CreateNotFound = outerTable.CreateNotFound
+	tableSnakeCase, tableUpperCamelCase, tableLowerCamelCase := util.FormatField(outerTable.Table)
+	table.SnakeCase = tableSnakeCase
+	table.UpperCamelCase = tableUpperCamelCase
+	table.LowerCamelCase = tableLowerCamelCase
+	fields := make([]*InnerField, 0)
+	var primaryField *InnerField
+	conflict := make(map[string]struct{})
+	var containsCache bool
+	for _, field := range outerTable.Fields {
+		if field.Cache && !containsCache {
+			containsCache = true
+		}
+		fieldSnakeCase, fieldUpperCamelCase, fieldLowerCamelCase := util.FormatField(field.Name)
+		tag, err := genTag(fieldSnakeCase)
+		if err != nil {
+			return nil, err
+		}
+		var comment string
+		if field.Comment != "" {
+			comment = fmt.Sprintf("// %s", field.Comment)
+		}
+		withFields := make([]InnerWithField, 0)
+		unique := make([]string, 0)
+		unique = append(unique, fmt.Sprintf("%v", field.QueryType))
+		unique = append(unique, field.Name)
+
+		for _, item := range field.WithFields {
+			unique = append(unique, item.Name)
+			withFieldSnakeCase, withFieldUpperCamelCase, withFieldLowerCamelCase := util.FormatField(item.Name)
+			withFields = append(withFields, InnerWithField{
+				Case: Case{
+					SnakeCase:      withFieldSnakeCase,
+					LowerCamelCase: withFieldLowerCamelCase,
+					UpperCamelCase: withFieldUpperCamelCase,
+				},
+				DataType: commonMysqlDataTypeMap[item.DataBaseType],
+			})
+		}
+		sort.Strings(unique)
+		uniqueKey := strings.Join(unique, "#")
+		if _, ok := conflict[uniqueKey]; ok {
+			return nil, ErrCircleQuery
+		} else {
+			conflict[uniqueKey] = struct{}{}
+		}
+		sortFields := make([]InnerSort, 0)
+		for _, sortField := range field.OuterSort {
+			sortSnake, sortUpperCamelCase, sortLowerCamelCase := util.FormatField(sortField.Field)
+			sortFields = append(sortFields, InnerSort{
+				Field: Case{
+					SnakeCase:      sortSnake,
+					LowerCamelCase: sortUpperCamelCase,
+					UpperCamelCase: sortLowerCamelCase,
+				},
+				Asc: sortField.Asc,
+			})
+		}
+		innerField := &InnerField{
+			IsPrimaryKey: field.IsPrimaryKey,
+			InnerWithField: InnerWithField{
+				Case: Case{
+					SnakeCase:      fieldSnakeCase,
+					LowerCamelCase: fieldLowerCamelCase,
+					UpperCamelCase: fieldUpperCamelCase,
+				},
+				DataType: commonMysqlDataTypeMap[field.DataBaseType],
+			},
+			DataBaseType: field.DataBaseType,
+			Tag:          tag,
+			Comment:      comment,
+			Cache:        field.Cache,
+			QueryType:    field.QueryType,
+			WithFields:   withFields,
+			Sort:         sortFields,
+		}
+		if field.IsPrimaryKey {
+			primaryField = innerField
+		}
+		fields = append(fields, innerField)
+	}
+	if primaryField == nil {
+		return nil, errors.New("please ensure that primary exists")
+	}
+	table.ContainsCache = containsCache
+	primaryField.Cache = containsCache
+	table.PrimaryField = primaryField
+	table.Fields = fields
+	cacheKey, err := genCacheKeys(&table)
+	if err != nil {
+		return nil, err
+	}
+	table.CacheKey = cacheKey
+	return &table, nil
+}

+ 51 - 0
tools/goctl/model/sql/gen/delete.go

@@ -0,0 +1,51 @@
+package gen
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genDelete(table *InnerTable) (string, error) {
+	t, err := template.New("delete").Parse(sqltemplate.Delete)
+	if err != nil {
+		return "", nil
+	}
+	deleteBuffer := new(bytes.Buffer)
+	keys := make([]string, 0)
+	keyValues := make([]string, 0)
+	for snake, key := range table.CacheKey {
+		if snake == table.PrimaryField.SnakeCase {
+			keys = append(keys, key.Key)
+		} else {
+			keys = append(keys, key.DataKey)
+		}
+		keyValues = append(keyValues, key.KeyVariable)
+	}
+	var isOnlyPrimaryKeyCache = true
+	for _, item := range table.Fields {
+		if item.IsPrimaryKey {
+			continue
+		}
+		if item.Cache {
+			isOnlyPrimaryKeyCache = false
+			break
+		}
+	}
+	err = t.Execute(deleteBuffer, map[string]interface{}{
+		"upperObject":     table.UpperCamelCase,
+		"containsCache":   table.ContainsCache,
+		"isNotPrimaryKey": !isOnlyPrimaryKeyCache,
+		"lowerPrimaryKey": table.PrimaryField.LowerCamelCase,
+		"dataType":        table.PrimaryField.DataType,
+		"keys":            strings.Join(keys, "\r\n"),
+		"snakePrimaryKey": table.PrimaryField.SnakeCase,
+		"keyValues":       strings.Join(keyValues, ", "),
+	})
+	if err != nil {
+		return "", err
+	}
+	return deleteBuffer.String(), nil
+}

+ 7 - 0
tools/goctl/model/sql/gen/error.go

@@ -0,0 +1,7 @@
+package gen
+
+import "errors"
+
+var (
+	ErrCircleQuery = errors.New("circle query with other fields")
+)

+ 39 - 0
tools/goctl/model/sql/gen/field.go

@@ -0,0 +1,39 @@
+package gen
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genFields(fields []*InnerField) (string, error) {
+	list := make([]string, 0)
+	for _, field := range fields {
+		result, err := genField(field)
+		if err != nil {
+			return "", err
+		}
+		list = append(list, result)
+	}
+	return strings.Join(list, "\r\n"), nil
+}
+
+func genField(field *InnerField) (string, error) {
+	t, err := template.New("types").Parse(sqltemplate.Field)
+	if err != nil {
+		return "", nil
+	}
+	var typeBuffer = new(bytes.Buffer)
+	err = t.Execute(typeBuffer, map[string]string{
+		"name":    field.UpperCamelCase,
+		"type":    field.DataType,
+		"tag":     field.Tag,
+		"comment": field.Comment,
+	})
+	if err != nil {
+		return "", err
+	}
+	return typeBuffer.String(), nil
+}

+ 55 - 0
tools/goctl/model/sql/gen/findallbyfield.go

@@ -0,0 +1,55 @@
+package gen
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genFindAllByField(table *InnerTable) (string, error) {
+	t, err := template.New("findAllByField").Parse(sqltemplate.FindAllByField)
+	if err != nil {
+		return "", err
+	}
+	list := make([]string, 0)
+	for _, field := range table.Fields {
+		if field.IsPrimaryKey {
+			continue
+		}
+		if field.QueryType != QueryAll {
+			continue
+		}
+		fineOneByFieldBuffer := new(bytes.Buffer)
+		upperFields := make([]string, 0)
+		in := make([]string, 0)
+		expressionFields := make([]string, 0)
+		expressionValuesFields := make([]string, 0)
+		upperFields = append(upperFields, field.UpperCamelCase)
+		in = append(in, field.LowerCamelCase+" "+field.DataType)
+		expressionFields = append(expressionFields, field.SnakeCase+" = ?")
+		expressionValuesFields = append(expressionValuesFields, field.LowerCamelCase)
+		for _, withField := range field.WithFields {
+			upperFields = append(upperFields, withField.UpperCamelCase)
+			in = append(in, withField.LowerCamelCase+" "+withField.DataType)
+			expressionFields = append(expressionFields, withField.SnakeCase+" = ?")
+			expressionValuesFields = append(expressionValuesFields, withField.LowerCamelCase)
+		}
+		err = t.Execute(fineOneByFieldBuffer, map[string]interface{}{
+			"in":               strings.Join(in, ","),
+			"upperObject":      table.UpperCamelCase,
+			"upperFields":      strings.Join(upperFields, "And"),
+			"lowerObject":      table.LowerCamelCase,
+			"snakePrimaryKey":  field.SnakeCase,
+			"expression":       strings.Join(expressionFields, " AND "),
+			"expressionValues": strings.Join(expressionValuesFields, ", "),
+			"containsCache":    table.ContainsCache,
+		})
+		if err != nil {
+			return "", err
+		}
+		list = append(list, fineOneByFieldBuffer.String())
+	}
+	return strings.Join(list, ""), nil
+}

+ 63 - 0
tools/goctl/model/sql/gen/findallbylimit.go

@@ -0,0 +1,63 @@
+package gen
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genFindLimitByField(table *InnerTable) (string, error) {
+	t, err := template.New("findLimitByField").Parse(sqltemplate.FindLimitByField)
+	if err != nil {
+		return "", err
+	}
+	list := make([]string, 0)
+	for _, field := range table.Fields {
+		if field.IsPrimaryKey {
+			continue
+		}
+		if field.QueryType != QueryLimit {
+			continue
+		}
+		fineOneByFieldBuffer := new(bytes.Buffer)
+		upperFields := make([]string, 0)
+		in := make([]string, 0)
+		expressionFields := make([]string, 0)
+		expressionValuesFields := make([]string, 0)
+		upperFields = append(upperFields, field.UpperCamelCase)
+		in = append(in, field.LowerCamelCase+" "+field.DataType)
+		expressionFields = append(expressionFields, field.SnakeCase+" = ?")
+		expressionValuesFields = append(expressionValuesFields, field.LowerCamelCase)
+		for _, withField := range field.WithFields {
+			upperFields = append(upperFields, withField.UpperCamelCase)
+			in = append(in, withField.LowerCamelCase+" "+withField.DataType)
+			expressionFields = append(expressionFields, withField.SnakeCase+" = ?")
+			expressionValuesFields = append(expressionValuesFields, withField.LowerCamelCase)
+		}
+		sortList := make([]string, 0)
+		for _, item := range field.Sort {
+			var sort = "ASC"
+			if !item.Asc {
+				sort = "DESC"
+			}
+			sortList = append(sortList, item.Field.SnakeCase+" "+sort)
+		}
+		err = t.Execute(fineOneByFieldBuffer, map[string]interface{}{
+			"in":               strings.Join(in, ","),
+			"upperObject":      table.UpperCamelCase,
+			"upperFields":      strings.Join(upperFields, "And"),
+			"lowerObject":      table.LowerCamelCase,
+			"expression":       strings.Join(expressionFields, " AND "),
+			"expressionValues": strings.Join(expressionValuesFields, ", "),
+			"sortExpression":   strings.Join(sortList, ","),
+			"containsCache":    table.ContainsCache,
+		})
+		if err != nil {
+			return "", err
+		}
+		list = append(list, fineOneByFieldBuffer.String())
+	}
+	return strings.Join(list, ""), nil
+}

+ 30 - 0
tools/goctl/model/sql/gen/findone.go

@@ -0,0 +1,30 @@
+package gen
+
+import (
+	"bytes"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genFindOne(table *InnerTable) (string, error) {
+	t, err := template.New("findOne").Parse(sqltemplate.FindOne)
+	if err != nil {
+		return "", err
+	}
+	fineOneBuffer := new(bytes.Buffer)
+	err = t.Execute(fineOneBuffer, map[string]interface{}{
+		"withCache":        table.PrimaryField.Cache,
+		"upperObject":      table.UpperCamelCase,
+		"lowerObject":      table.LowerCamelCase,
+		"snakePrimaryKey":  table.PrimaryField.SnakeCase,
+		"lowerPrimaryKey":  table.PrimaryField.LowerCamelCase,
+		"dataType":         table.PrimaryField.DataType,
+		"cacheKey":         table.CacheKey[table.PrimaryField.SnakeCase].Key,
+		"cacheKeyVariable": table.CacheKey[table.PrimaryField.SnakeCase].KeyVariable,
+	})
+	if err != nil {
+		return "", err
+	}
+	return fineOneBuffer.String(), nil
+}

+ 67 - 0
tools/goctl/model/sql/gen/fineonebyfield.go

@@ -0,0 +1,67 @@
+package gen
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genFineOneByField(table *InnerTable) (string, error) {
+	t, err := template.New("findOneByField").Parse(sqltemplate.FindOneByField)
+	if err != nil {
+		return "", err
+	}
+	list := make([]string, 0)
+	for _, field := range table.Fields {
+		if field.IsPrimaryKey {
+			continue
+		}
+		if field.QueryType != QueryOne {
+			continue
+		}
+		fineOneByFieldBuffer := new(bytes.Buffer)
+		upperFields := make([]string, 0)
+		in := make([]string, 0)
+		expressionFields := make([]string, 0)
+		expressionValuesFields := make([]string, 0)
+		upperFields = append(upperFields, field.UpperCamelCase)
+		in = append(in, field.LowerCamelCase+" "+field.DataType)
+		expressionFields = append(expressionFields, field.SnakeCase+" = ?")
+		expressionValuesFields = append(expressionValuesFields, field.LowerCamelCase)
+		for _, withField := range field.WithFields {
+			upperFields = append(upperFields, withField.UpperCamelCase)
+			in = append(in, withField.LowerCamelCase+" "+withField.DataType)
+			expressionFields = append(expressionFields, withField.SnakeCase+" = ?")
+			expressionValuesFields = append(expressionValuesFields, withField.LowerCamelCase)
+		}
+		err = t.Execute(fineOneByFieldBuffer, map[string]interface{}{
+			"in":                    strings.Join(in, ","),
+			"upperObject":           table.UpperCamelCase,
+			"upperFields":           strings.Join(upperFields, "And"),
+			"onlyOneFiled":          len(field.WithFields) == 0,
+			"withCache":             field.Cache,
+			"containsCache":         table.ContainsCache,
+			"lowerObject":           table.LowerCamelCase,
+			"lowerField":            field.LowerCamelCase,
+			"snakeField":            field.SnakeCase,
+			"lowerPrimaryKey":       table.PrimaryField.LowerCamelCase,
+			"UpperPrimaryKey":       table.PrimaryField.UpperCamelCase,
+			"primaryKeyDefine":      table.CacheKey[table.PrimaryField.SnakeCase].Define,
+			"primarySnakeField":     table.PrimaryField.SnakeCase,
+			"primaryDataType":       table.PrimaryField.DataType,
+			"primaryDataTypeString": table.PrimaryField.DataType == "string",
+			"upperObjectKey":        table.PrimaryField.UpperCamelCase,
+			"cacheKey":              table.CacheKey[field.SnakeCase].Key,
+			"cacheKeyVariable":      table.CacheKey[field.SnakeCase].KeyVariable,
+			"expression":            strings.Join(expressionFields, " AND "),
+			"expressionValues":      strings.Join(expressionValuesFields, ", "),
+		})
+		if err != nil {
+			return "", err
+		}
+		list = append(list, fineOneByFieldBuffer.String())
+	}
+	return strings.Join(list, ""), nil
+}

+ 23 - 0
tools/goctl/model/sql/gen/imports.go

@@ -0,0 +1,23 @@
+package gen
+
+import (
+	"bytes"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genImports(table *InnerTable) (string, error) {
+	t, err := template.New("imports").Parse(sqltemplate.Imports)
+	if err != nil {
+		return "", err
+	}
+	importBuffer := new(bytes.Buffer)
+	err = t.Execute(importBuffer, map[string]interface{}{
+		"containsCache": table.ContainsCache,
+	})
+	if err != nil {
+		return "", err
+	}
+	return importBuffer.String(), nil
+}

+ 37 - 0
tools/goctl/model/sql/gen/insert.go

@@ -0,0 +1,37 @@
+package gen
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genInsert(table *InnerTable) (string, error) {
+	t, err := template.New("insert").Parse(sqltemplate.Insert)
+	if err != nil {
+		return "", nil
+	}
+	insertBuffer := new(bytes.Buffer)
+	expressions := make([]string, 0)
+	expressionValues := make([]string, 0)
+	for _, filed := range table.Fields {
+		if filed.SnakeCase == "create_time" || filed.SnakeCase == "update_time" || filed.IsPrimaryKey {
+			continue
+		}
+		expressions = append(expressions, "?")
+		expressionValues = append(expressionValues, "data."+filed.UpperCamelCase)
+	}
+	err = t.Execute(insertBuffer, map[string]interface{}{
+		"upperObject":      table.UpperCamelCase,
+		"lowerObject":      table.LowerCamelCase,
+		"expression":       strings.Join(expressions, ", "),
+		"expressionValues": strings.Join(expressionValues, ", "),
+		"containsCache":    table.ContainsCache,
+	})
+	if err != nil {
+		return "", err
+	}
+	return insertBuffer.String(), nil
+}

+ 106 - 0
tools/goctl/model/sql/gen/keys.go

@@ -0,0 +1,106 @@
+package gen
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+)
+
+var (
+	cacheKeyExpressionTemplate = `cache{{.upperCamelTable}}{{.upperCamelField}}Prefix = "cache#{{.lowerCamelTable}}#{{.lowerCamelField}}#"`
+	keyTemplate                = `{{.lowerCamelField}}Key := fmt.Sprintf("%s%v", {{.define}}, {{.lowerCamelField}})`
+	keyRespTemplate            = `{{.lowerCamelField}}Key := fmt.Sprintf("%s%v", {{.define}}, resp.{{.upperCamelField}})`
+	keyDataTemplate            = `{{.lowerCamelField}}Key := fmt.Sprintf("%s%v", {{.define}}, data.{{.upperCamelField}})`
+)
+
+type (
+	Key struct {
+		Define      string // cacheKey define,如:cacheUserUserIdPrefix
+		Value       string // cacheKey value expression,如:cache#user#userId#
+		Expression  string // cacheKey expression,如:cacheUserUserIdPrefix="cache#user#userId#"
+		KeyVariable string // cacheKey 声明变量,如:userIdKey
+		Key         string // 缓存key的代码,如 userIdKey:=fmt.Sprintf("%s%v", cacheUserUserIdPrefix, userId)
+		DataKey     string // 缓存key的代码,如 userIdKey:=fmt.Sprintf("%s%v", cacheUserUserIdPrefix, data.userId)
+		RespKey     string // 缓存key的代码,如 userIdKey:=fmt.Sprintf("%s%v", cacheUserUserIdPrefix, resp.userId)
+	}
+)
+
+// key-数据库原始字段名,value-缓存key对象
+func genCacheKeys(table *InnerTable) (map[string]Key, error) {
+	fields := table.Fields
+	var m = make(map[string]Key)
+	if !table.ContainsCache {
+		return m, nil
+	}
+	for _, field := range fields {
+		if !field.Cache && !field.IsPrimaryKey {
+			continue
+		}
+		t, err := template.New("keyExpression").Parse(cacheKeyExpressionTemplate)
+		if err != nil {
+			return nil, err
+		}
+		var expressionBuffer = new(bytes.Buffer)
+		err = t.Execute(expressionBuffer, map[string]string{
+			"upperCamelTable": table.UpperCamelCase,
+			"lowerCamelTable": table.LowerCamelCase,
+			"upperCamelField": field.UpperCamelCase,
+			"lowerCamelField": field.LowerCamelCase,
+		})
+		if err != nil {
+			return nil, err
+		}
+		expression := expressionBuffer.String()
+		expressionAr := strings.Split(expression, "=")
+		define := strings.TrimSpace(expressionAr[0])
+		value := strings.TrimSpace(expressionAr[1])
+		t, err = template.New("key").Parse(keyTemplate)
+		if err != nil {
+			return nil, err
+		}
+		var keyBuffer = new(bytes.Buffer)
+		err = t.Execute(keyBuffer, map[string]string{
+			"lowerCamelField": field.LowerCamelCase,
+			"define":          define,
+		})
+		if err != nil {
+			return nil, err
+		}
+		t, err = template.New("keyData").Parse(keyDataTemplate)
+		if err != nil {
+			return nil, err
+		}
+		var keyDataBuffer = new(bytes.Buffer)
+		err = t.Execute(keyDataBuffer, map[string]string{
+			"lowerCamelField": field.LowerCamelCase,
+			"upperCamelField": field.UpperCamelCase,
+			"define":          define,
+		})
+		if err != nil {
+			return nil, err
+		}
+		t, err = template.New("keyResp").Parse(keyRespTemplate)
+		if err != nil {
+			return nil, err
+		}
+		var keyRespBuffer = new(bytes.Buffer)
+		err = t.Execute(keyRespBuffer, map[string]string{
+			"lowerCamelField": field.LowerCamelCase,
+			"upperCamelField": field.UpperCamelCase,
+			"define":          define,
+		})
+		if err != nil {
+			return nil, err
+		}
+		m[field.SnakeCase] = Key{
+			Define:      define,
+			Value:       value,
+			Expression:  expression,
+			KeyVariable: field.LowerCamelCase + "Key",
+			Key:         keyBuffer.String(),
+			DataKey:     keyDataBuffer.String(),
+			RespKey:     keyRespBuffer.String(),
+		}
+	}
+	return m, nil
+}

+ 100 - 0
tools/goctl/model/sql/gen/keys_test.go

@@ -0,0 +1,100 @@
+package gen
+
+import (
+	"log"
+	"testing"
+
+	"zero/core/logx"
+)
+
+func TestKeys(t *testing.T) {
+	var table = OuterTable{
+		Table:          "user_info",
+		CreateNotFound: true,
+		Fields: []*OuterFiled{
+			{
+				IsPrimaryKey: true,
+				Name:         "user_id",
+				DataBaseType: "bigint",
+				Comment:      "主键id",
+			},
+			{
+				Name:         "campus_id",
+				DataBaseType: "bigint",
+				Comment:      "整校id",
+				QueryType:    QueryAll,
+				Cache:        false,
+			},
+			{
+				Name:         "name",
+				DataBaseType: "varchar",
+				Comment:      "用户姓名",
+				QueryType:    QueryOne,
+			},
+			{
+				Name:         "id_number",
+				DataBaseType: "varchar",
+				Comment:      "身份证",
+				Cache:        false,
+				QueryType:    QueryNone,
+				WithFields: []OuterWithField{
+					{
+						Name:         "name",
+						DataBaseType: "varchar",
+					},
+				},
+			},
+			{
+				Name:         "age",
+				DataBaseType: "int",
+				Comment:      "年龄",
+				Cache:        false,
+				QueryType:    QueryNone,
+			},
+			{
+				Name:         "gender",
+				DataBaseType: "tinyint",
+				Comment:      "性别,0-男,1-女,2-不限",
+				QueryType:    QueryLimit,
+				WithFields: []OuterWithField{
+					{
+						Name:         "campus_id",
+						DataBaseType: "bigint",
+					},
+				},
+				OuterSort: []OuterSort{
+					{
+						Field: "create_time",
+						Asc:   false,
+					},
+				},
+			},
+			{
+				Name:         "mobile",
+				DataBaseType: "varchar",
+				Comment:      "手机号",
+				QueryType:    QueryOne,
+				Cache:        true,
+			},
+			{
+				Name:         "create_time",
+				DataBaseType: "timestamp",
+				Comment:      "创建时间",
+			},
+			{
+				Name:         "update_time",
+				DataBaseType: "timestamp",
+				Comment:      "更新时间",
+			},
+		},
+	}
+	innerTable, err := TableConvert(table)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	tp, err := GenModel(innerTable)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	logx.Info(tp)
+}

+ 86 - 0
tools/goctl/model/sql/gen/model.go

@@ -0,0 +1,86 @@
+package gen
+
+import (
+	"bytes"
+	"go/format"
+	"strings"
+	"text/template"
+
+	"zero/core/logx"
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func GenModel(table *InnerTable) (string, error) {
+	t, err := template.New("model").Parse(sqltemplate.Model)
+	if err != nil {
+		return "", nil
+	}
+	modelBuffer := new(bytes.Buffer)
+	importsCode, err := genImports(table)
+	if err != nil {
+		return "", err
+	}
+	varsCode, err := genVars(table)
+	if err != nil {
+		return "", err
+	}
+	typesCode, err := genTypes(table)
+	if err != nil {
+		return "", err
+	}
+	newCode, err := genNew(table)
+	if err != nil {
+		return "", err
+	}
+	insertCode, err := genInsert(table)
+	if err != nil {
+		return "", err
+	}
+	var findCode = make([]string, 0)
+	findOneCode, err := genFindOne(table)
+	if err != nil {
+		return "", err
+	}
+	findOneByFieldCode, err := genFineOneByField(table)
+	if err != nil {
+		return "", err
+	}
+	findAllCode, err := genFindAllByField(table)
+	if err != nil {
+		return "", err
+	}
+	findLimitCode, err := genFindLimitByField(table)
+	if err != nil {
+		return "", err
+	}
+	findCode = append(findCode, findOneCode, findOneByFieldCode, findAllCode, findLimitCode)
+	updateCode, err := genUpdate(table)
+	if err != nil {
+		return "", err
+	}
+	deleteCode, err := genDelete(table)
+	if err != nil {
+		return "", err
+	}
+
+	err = t.Execute(modelBuffer, map[string]interface{}{
+		"imports": importsCode,
+		"vars":    varsCode,
+		"types":   typesCode,
+		"new":     newCode,
+		"insert":  insertCode,
+		"find":    strings.Join(findCode, "\r\n"),
+		"update":  updateCode,
+		"delete":  deleteCode,
+	})
+	if err != nil {
+		return "", err
+	}
+	result := modelBuffer.String()
+	bts, err := format.Source([]byte(result))
+	if err != nil {
+		logx.Errorf("%+v", err)
+		return "", err
+	}
+	return string(bts), nil
+}

+ 24 - 0
tools/goctl/model/sql/gen/new.go

@@ -0,0 +1,24 @@
+package gen
+
+import (
+	"bytes"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genNew(table *InnerTable) (string, error) {
+	t, err := template.New("new").Parse(sqltemplate.New)
+	if err != nil {
+		return "", err
+	}
+	newBuffer := new(bytes.Buffer)
+	err = t.Execute(newBuffer, map[string]interface{}{
+		"containsCache": table.ContainsCache,
+		"upperObject":   table.UpperCamelCase,
+	})
+	if err != nil {
+		return "", err
+	}
+	return newBuffer.String(), nil
+}

+ 99 - 0
tools/goctl/model/sql/gen/shared.go

@@ -0,0 +1,99 @@
+package gen
+
+var (
+	commonMysqlDataTypeMap = map[string]string{
+		"tinyint":    "int64",
+		"smallint":   "int64",
+		"mediumint":  "int64",
+		"int":        "int64",
+		"integer":    "int64",
+		"bigint":     "int64",
+		"float":      "float64",
+		"double":     "float64",
+		"decimal":    "float64",
+		"date":       "time.Time",
+		"time":       "string",
+		"year":       "int64",
+		"datetime":   "time.Time",
+		"timestamp":  "time.Time",
+		"char":       "string",
+		"varchar":    "string",
+		"tinyblob":   "string",
+		"tinytext":   "string",
+		"blob":       "string",
+		"text":       "string",
+		"mediumblob": "string",
+		"mediumtext": "string",
+		"longblob":   "string",
+		"longtext":   "string",
+	}
+)
+
+const (
+	QueryNone  QueryType = 0
+	QueryOne   QueryType = 1 // 仅支持单个字段为查询条件
+	QueryAll   QueryType = 2 // 可支持多个字段为查询条件,且关系均为and
+	QueryLimit QueryType = 3 // 可支持多个字段为查询条件,且关系均为and
+)
+
+type (
+	QueryType int
+
+	Case struct {
+		SnakeCase      string
+		LowerCamelCase string
+		UpperCamelCase string
+	}
+	InnerWithField struct {
+		Case
+		DataType string
+	}
+	InnerTable struct {
+		Case
+		ContainsCache  bool
+		CreateNotFound bool
+		PrimaryField   *InnerField
+		Fields         []*InnerField
+		CacheKey       map[string]Key // key-数据库字段
+	}
+	InnerField struct {
+		IsPrimaryKey bool
+		InnerWithField
+		DataBaseType string // 数据库中字段类型
+		Tag          string // 标签,格式:`db:"xxx"`
+		Comment      string // 注释,以"// 开头"
+		Cache        bool   // 是否缓存模式
+		QueryType    QueryType
+		WithFields   []InnerWithField
+		Sort         []InnerSort
+	}
+	InnerSort struct {
+		Field Case
+		Asc   bool
+	}
+
+	OuterTable struct {
+		Table          string        `json:"table"`
+		CreateNotFound bool          `json:"createNotFound,optional"`
+		Fields         []*OuterFiled `json:"fields"`
+	}
+	OuterWithField struct {
+		Name         string `json:"name"`
+		DataBaseType string `json:"dataBaseType"`
+	}
+	OuterSort struct {
+		Field string `json:"fields"`
+		Asc   bool   `json:"asc,optional"`
+	}
+	OuterFiled struct {
+		IsPrimaryKey bool   `json:"isPrimaryKey,optional"`
+		Name         string `json:"name"`
+		DataBaseType string `json:"dataBaseType"`
+		Comment      string `json:"comment"`
+		Cache        bool   `json:"cache,optional"`
+		// if IsPrimaryKey==false下面字段有效
+		QueryType  QueryType        `json:"queryType"`           // 查找类型
+		WithFields []OuterWithField `json:"withFields,optional"` // 其他字段联合组成条件的字段列表
+		OuterSort  []OuterSort      `json:"sort,optional"`
+	}
+)

+ 26 - 0
tools/goctl/model/sql/gen/tag.go

@@ -0,0 +1,26 @@
+package gen
+
+import (
+	"bytes"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genTag(in string) (string, error) {
+	if in == "" {
+		return in, nil
+	}
+	t, err := template.New("tag").Parse(sqltemplate.Tag)
+	if err != nil {
+		return "", err
+	}
+	var tagBuffer = new(bytes.Buffer)
+	err = t.Execute(tagBuffer, map[string]interface{}{
+		"field": in,
+	})
+	if err != nil {
+		return "", err
+	}
+	return tagBuffer.String(), nil
+}

+ 30 - 0
tools/goctl/model/sql/gen/types.go

@@ -0,0 +1,30 @@
+package gen
+
+import (
+	"bytes"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genTypes(table *InnerTable) (string, error) {
+	fields := table.Fields
+	t, err := template.New("types").Parse(sqltemplate.Types)
+	if err != nil {
+		return "", nil
+	}
+	var typeBuffer = new(bytes.Buffer)
+	fieldsString, err := genFields(fields)
+	if err != nil {
+		return "", err
+	}
+	err = t.Execute(typeBuffer, map[string]interface{}{
+		"upperObject":   table.UpperCamelCase,
+		"containsCache": table.ContainsCache,
+		"fields":        fieldsString,
+	})
+	if err != nil {
+		return "", err
+	}
+	return typeBuffer.String(), nil
+}

+ 38 - 0
tools/goctl/model/sql/gen/update.go

@@ -0,0 +1,38 @@
+package gen
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genUpdate(table *InnerTable) (string, error) {
+	t, err := template.New("update").Parse(sqltemplate.Update)
+	if err != nil {
+		return "", nil
+	}
+	updateBuffer := new(bytes.Buffer)
+	expressionValues := make([]string, 0)
+	for _, filed := range table.Fields {
+		if filed.SnakeCase == "create_time" || filed.SnakeCase == "update_time" || filed.IsPrimaryKey {
+			continue
+		}
+		expressionValues = append(expressionValues, "data."+filed.UpperCamelCase)
+	}
+	expressionValues = append(expressionValues, "data."+table.PrimaryField.UpperCamelCase)
+	err = t.Execute(updateBuffer, map[string]interface{}{
+		"containsCache":      table.ContainsCache,
+		"upperObject":        table.UpperCamelCase,
+		"primaryCacheKey":    table.CacheKey[table.PrimaryField.SnakeCase].DataKey,
+		"primaryKeyVariable": table.CacheKey[table.PrimaryField.SnakeCase].KeyVariable,
+		"lowerObject":        table.LowerCamelCase,
+		"primarySnakeCase":   table.PrimaryField.SnakeCase,
+		"expressionValues":   strings.Join(expressionValues, ", "),
+	})
+	if err != nil {
+		return "", err
+	}
+	return updateBuffer.String(), nil
+}

+ 36 - 0
tools/goctl/model/sql/gen/vars.go

@@ -0,0 +1,36 @@
+package gen
+
+import (
+	"bytes"
+	"strings"
+	"text/template"
+
+	sqltemplate "zero/tools/goctl/model/sql/template"
+)
+
+func genVars(table *InnerTable) (string, error) {
+	t, err := template.New("vars").Parse(sqltemplate.Vars)
+	if err != nil {
+		return "", err
+	}
+	varBuffer := new(bytes.Buffer)
+	m, err := genCacheKeys(table)
+	if err != nil {
+		return "", err
+	}
+	keys := make([]string, 0)
+	for _, v := range m {
+		keys = append(keys, v.Expression)
+	}
+	err = t.Execute(varBuffer, map[string]interface{}{
+		"lowerObject":     table.LowerCamelCase,
+		"upperObject":     table.UpperCamelCase,
+		"createNotFound":  table.CreateNotFound,
+		"keysDefine":      strings.Join(keys, "\r\n"),
+		"snakePrimaryKey": table.PrimaryField.SnakeCase,
+	})
+	if err != nil {
+		return "", err
+	}
+	return varBuffer.String(), nil
+}

Some files were not shown because too many files changed in this diff