Vscode niuhe(牛河/河粉) 插件是一款面向 golang 的后端 IDL 代码生成(翻译)插件, 旨在实现前后端一致的 API设计, 开发测试一体化协作开发流程, 提高开发效率. 它通过简单的 niuhe idl 语法 定义接口, 支持生成对应的 go 服务代码, 前端 api 定义, swagger 文档, 以及其他语言的协议.

niuhe IDl 是一种类似于 Python 的对象接口定义语言(Interface Definition Language, IDL),语法简洁,开发者易上手。

插件借助对 niuhe IDl 的支持与拓展,开发者只需定义一次接口就能多处复用,避免前后端重复定义,接口更新也无需繁琐通知。此外,插件还能自动生成 CURD 相关前后端代码,前端代码完美适配管理后台模板, 轻松实现表定义, 数据增删改查和展示等基本功能。

niuhe 插件通过一套接口定义生成多套一致代码,可减少重复劳动,提升开发效率,提升项目的一致性和可维护性,让团队聚焦核心业务,非常适合中小开发团队使用。

核心功能

功能模块核心能力适用场景
Go语言支持自动生成服务框架代码:包含路由管理niuhe、请求参数解析zpform、响应格式处理等基础代码niuheniuhezpform 两个库是本插件的基石后端开发
TypeScript支持一键生成前端API调用代码,支持Web/小程序/React Native等场景前端开发
文档生成自动生成Swagger文档,支持导入Postman/Apifox等测试工具 【示例】, 接入 MCP接口测试
多语言协议生成标准化协议文件,支持自定义转换到其他编程语言前端开发

Go服务核心特性

功能优势
智能路由自动根据文件结构生成路由配置,无需手动维护
参数处理内置请求参数校验和响应格式化,专注业务逻辑开发
XORM集成自动生成数据库操作代码(表结构定义、DAO层、服务层)
CURD操作自动生成CURD API逻辑代码
常量管理统一管理业务常量,提升代码可维护性

配套解决方案

开提供开箱即用的管理系统模板,加速企业级应用开发:

解决方案亮点
Admin-Core内置RBAC权限体系,快速实现用户/角色/菜单管理
Vue3管理模板基于流行技术栈(Vue3+Element Plus),提供完整后台功能组件, 同时支持代码前端页面代码生成

实践案例

快速开始

本章节将介绍如何安装与激活插件,实现一个 Hellow World 并介绍 niuhe 语法

本教程示例代码库: https://github.com/ma-guo/niuhe-mdbook

1. 安装与激活

  1. VSCode 插件市场搜索 niuhe
  2. 点击安装后,可通过下列四种方式激活插件生成代码
    • 方式1: 点击资源管理器顶部出现的 icon 图标
    • 方式2: 点击工具栏中的 icon 图标
    • 方式3: 在资源管理器中选中任意文件并右键中 niuhe idl 生成
    • 方式4: Command + Shift + P 快捷键中 输入 niuhe 查找 niuhe idl 生成
方式1和3方式 2方式4
image.pngimage.pngimage.png

上图示例是旧版图标 </>, 新版本图标已经更换为 icon

2. Hello World 实战

本教程代码库为 niuhe-mdbook

在项目根目录创建:

#app=demo

class HelloReq():
    '''测试请求'''
    name = required.String(desc='用户名')

class HelloResp(Message):
   '''测试响应'''
    greeting = required.String(desc='问候语')

with services():
    GET('示例接口', '/api/hello/world/', HelloReq, HelloResp)

点击 icon 生成代码后,将自动创建如下文件:

.
├── README.md // 项目说明
├── conf
│   └── demo.yaml // 配置文件
├── makefile // linux/unix makefile 脚本
├── niuhe
│   └── all.niuhe // 项目入口文件
├── niuhe.log
└── src
    └── demo
        ├── app
        │   ├── api
        │   │   ├── protos
        │   │   │   └── gen_protos.go // 自定生成的 go 请求和响应结构定义文件
        │   │   └── views
        │   │       ├── gen_hello_views.go // 自定义生成的 go 路由处理器文件
        │   │       ├── hello_views.go // hello 香港 view 的实现文件
        │   │       └── init.go
        │   └── common
        │       └── consts
        │           └── gen_consts.go // 常量定义文件
        ├── config
        │   └── config.go // 配置文件定义, 读取 conf/demo.yaml 内容
        ├── go.mod
        ├── main.go // 项目入口
        └── xorm
            ├── daos
            │   └── init.go // dao 层基本定义定义
            ├── models
            │   └── models.go // model 层定义示例
            └── services
                └── init.go // service 层基本定义

接下来我们跟随 README.md 指引,执行下列命令运行项目

cd src/demo && go mod init demo && go mod tidy && go mod vendor && cd ../../ && make run

此时我们在浏览器访问链接: http://localhost:19999/api/hello/world/, 即可看到返回的问候语。

{"message":"Name(必填)","result":-1}

由此我们完成了一个简单的 Hello World 示例。

niuhe 语法

niuhe idl 简称 niuhe, 是智品网络后端团队开发的一门接口定义语言。

语法主要包括 注释, 常量(枚举), 数据类型(class/struct), 接口定义(路由), 数据字段定义五个部分。 语法示例图 niuhe 语法生成代码示意图

插件自定义了 .niuhe 后缀的代码高亮、代码提示和补全,也可选择 python 来做语法高亮

应用名称

应用名称需定义在入口文件 niuhe/all.niuhe 中定义,使用 #app=应用名 来定义。

#app=demo

注释

注释同 python 相同, 单行注释使用 #, 多行注释使用三个引号 ''', 类和常量的说明用注释的方式写在类定义的下一行。 当多行注释出现在 class 后时注释将作为 class 的说明

常量(枚举)

class Language(ConstGroup):
    '''语言枚举类'''
    ZH = Item("zh", "中文")
    EN = Item("en", "英文")

class LanguageType(ConstGroup):
    '''语言类型枚举'''
    ZH_CN = Item(1, "简体中文")
    ZH_TW = Item(2, "繁体中文")

# 下面为新语法定义 ConstGroup
# 新语法中支持通过属性名指定属性值, 支持 value, desc 和 depr 三个属性, 可混用
# depr 可出现在字段和类上, 表示该字段或类已废弃
class LanguageType(ConstGroup, depr=True):
    '''语言类型枚举'''
    ZH_CN = Item(value=1, desc="简体中文")
    ZH_TW = Item(2, desc="繁体中文")
    EN = Item(3, desc="英文", depr=True)
    FRANCE = Item(4, "法语", depr=True)

常量定义以 class 开头, 继承 ConstGroup, 内部通过 Item 来定义具体的常量值。常量仅支持 StringInteger 类型,且必须指定一个唯一标识符和一个显示名称。上述定义生成后的代码(src/demo/app/common/consts/gen_consts.go)为

package consts

// Generated by niuhe.idl

import "github.com/ma-guo/niuhe"

var Language struct {
	*niuhe.StringConstGroup
	ZH niuhe.StringConstItem `name:"中文" value:"zh"` // value: zh, name: 中文
	EN niuhe.StringConstItem `name:"英文" value:"en"` // value: en, name: 英文
}

// 语言类型枚举
var LanguageType struct {
	*niuhe.IntConstGroup
	ZH_CN niuhe.IntConstItem `name:"简体中文" value:"1"` // value: 1, name: 简体中文
	ZH_TW niuhe.IntConstItem `name:"繁体中文" value:"2"` // value: 2, name: 繁体中文
}

func init() {
	niuhe.InitConstGroup(&Language)
	niuhe.InitIntConstGroup(&LanguageType)
}

数据类型(class/struct)

在上一节 hello world 实战中,我们定义了一个接口的入参和出参类

class HelloReq():
    '''测试请求'''
    name = required.String(desc='用户名')

class HelloRsp(Message):
   '''测试响应'''
    greeting = required.String(desc='问候语')

# 可在 class 中通过 desc 和 depr 来定义类的描述和废弃状态
class HelloReq(depr=True, desc='测试请求', ver='1.1', used=True):
    name = required.String(desc='用户名')

数据类型定义以 class 开头, 继承 Message, Message 可不写, 类后跟随以注释形式的类说明(可选)和字段定义。 当类无字段时以 pass 做标记即可,可如下:

class HelloReq():
    '''测试请求, 无参数'''
    pass

上述定义生成的 struct 代码为:

package protos

// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改

// 测试请求
type HelloReq struct {
	Name string `json:"name" zpf_name:"name" zpf_reqd:"true"` //	用户名
}

// 测试响应
type HelloRsp struct {
	Greeting string `json:"greeting" zpf_name:"greeting" zpf_reqd:"true"` //	问候语
}

类的继承

类继承自Message可以继承自自定义的其他类, 通过在类名后添加括号的方式实现:

class NihaoReq(HelloReq):
    '''你好请求'''
    mingzi = required.String(desc='名字')

生成的 struct 代码为:

// 你好请求
type NihaoReq struct {
	Name   string `json:"name" zpf_name:"name" zpf_reqd:"true"`     //	用户名
	Mingzi string `json:"mingzi" zpf_name:"mingzi" zpf_reqd:"true"` //	名字
}

字段定义

字段定于语法为:

  member_name  = label.type(desc='', cls=..., group=..., ...)

其中

  • label 可以是 required(必填的), optional(可选的), repeated(重复的/数组) 三种
  • type 可以是 Integer, Decimal, Float, Long, String, Boolean, Message, Enum,StringEnum, File, Any 11种数据类型。

这里 requiredoptional 仅对入参有意义, 出参在 go 中无此限制。 repeated 在入参中同optional

字段可选属性有 desc, group, cls, depr, ver, demo, json,omit, value, reg, minlen, maxlen, minnum, maxnum, format 十五个属性。它们的说明如下

  • desc: 描述信息, 可选, 建议填写字段说明, 否则失去文档定义语言意义。
  • group: 指定 enumgroup, 仅当 typeEnumStringEnum 时有效和必填, 如 lang=required.Enum(desc='语言枚举', group=LanguageType)
  • cls: 指定类的类型,仅当 typeMessage 时有效和必填,如 message=optional.Message(desc='消息体',cls=HelloReq)
  • depr: depr=True/depr=true,将字段标记为 deprecated, 这在 docs 文档中有体现, 如导入到 apifox 中时字段将被标记为废弃(删除线), 便于同前端进行沟通.
  • ver: 指定字段的版本号, 如 ver='1.0.0。新加字段时建议标记,同 depr 一样,主要用于前后端沟通
  • demo: 指定字段的示例值, 如 demo=1, 这里赋予 demo 值的类型要同 type 字段标记的一致
  • json: 修改字段对应的 json 名, 如 json='name', 默认为空.
  • omit: 指定字段为 omitempty, 如 omit=True; 通常情况下,结构体中的零值字段(如int的0,string的"",bool的false等)会被序列化为JSON,即使它们没有被明确地设置。如果你不希望包含这些零值字段,可指定字段的 omit.
  • value: 定义 ConstGroup 时指定其值, 其他类型中无实际意义
  • reg: typeString 时验证其正则表达式
  • minlen: typeString 时验证其最小长度; 如 minlen=1
  • maxlen: typeString 时验证其最大长度; 如 maxlen=10
  • minnum: type 数字时验证其最小值; 如 minnum=1
  • maxnum: type 数字时验证其最大值; 如 maxnum=10
  • format: 用于指定日期格式, 如 format="2006-01-02"

实际上记住 desc, cls, group 三个即可,其他的 12 个是本插件拓展属性,不是高频使用属性

attrsreg, minlen, maxlen, minnum, maxnum 的属性请参考 zpform struct 定义

接口定义(路由)

接口定义以 with services(): 为开始标记, 后面跟随一个或多个路由定义,每个路由定义如下:

http_method('接口说明', 'api_path(/mode/view/method/)', 入参类名(可选), 出参类名(可选)[, author='xxx', status=tested])

其中

  • http_method 可以是 GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS 等 http method
  • 接口说明: 接口的简要描述, 如 '获取用户信息'
  • api_path: 必须是三段式 api 的路径, 如 /api/hello/world/
  • 入参类名: 可选, 如 HelloReq 当不填写时,得自己处理入参和出参的解析, 入参和出参得同时可选或同时必填。
  • 出参类名: 可选, 如 HelloRsp 当不填写时,得自己处理入参和出参的解析, 入参和出参得同时可选或同时必填。
  • author apifox 支持, 指定接口作者/责任人, 字符串
  • status apifox 支持, 指定 API状态, 可选值 designing|pending|developing|integrating|testing|tested|released|deprecated:exception:obsolete

本章节第二小节定义的路由如下:

with services():
    GET('示例接口', '/api/hello/world/', NihaoReq, HelloRsp)

生成的代码在 src/demo/app/api/views/[gen_]hello_views.go 文件中。

[] 表示可选, 下同

RPC 方式定义接口

在定义接口时,可以通过 RPC 关键字来定义一个接口。语法如下:

RPC('接口说明', [http_method/url=]'api_path(/mode/view/method/)', [author='xxx', status=tested])[.args(...)][.returns(...)][.codes(...)]

其中:

  • http_method/url=: 可选,指定 http 方法和路径,如 get='/api/hello/world/', url='/api/hello/world/' 或者直接写 '/api/hello/world/', 此时 http_methodGETPOST, http_method 可选值为 GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, 因此 RPC 方式中, 请求方法有九种定义方式。
  • .args(...): 可选,指定接口的入参,如 .args(name=required.String(desc='用户名'),...)
  • .returns(...): 可选,指定接口的出参,如 .returns(greeting=required.String(desc='问候语'),...)
  • .codes(...): 可选,指定接口的状态码和描述, 可用于约束 api 可返回的错误状态码范围,如 .codes(LanguageType.ZH_CN, LanguageType.ZH_TW, Item(100, '错误描述')), codes 中为定义的 Integer 枚举值。服务端返回值检测请参考 错误代码定义和检测
  • author apifox 支持, 指定接口作者/责任人, 字符串
  • status apifox 支持, 指定 API状态, 可选值 designing|pending|developing|integrating|testing|tested|released|deprecated:exception:obsolete

RPC 方式同 GET, POST 的差异是不用直接定义一个 class, 在一个文件中定义过多 class 时不便于一目了然地查阅接口的入参和出参。

同时 RPC 方式支持只定义入参和出参的情况, 此时会生成一个空结构来填补另一个未定义的参数结构

include 引用

在定义类时,可以通过 include 关键字引用其他文件中的类. 如在 comm.niuhe 文件中定义了如下内容:

# 公共类型定义

class NoneResp():
    '''空响应'''
    pass

class NoneReq():
    '''空请求'''
    pass

此时可在 *.niuhe 文件中通过 include 引入:

include('comm.niuhe')
# 然后通过 comm.NoneReq, comm.NoneResp 来使用这些类
with services():
    GET('include 引用示例', '/api/include/demo/', comm.NoneReq, comm.NoneResp)

include 支持跨文件引用其他目录中的 *.niuhe 文件

其他说明

入参参数中, 并不支持解析复杂结构, 如 Message, Any, Dict 等. 而出参则无此限制, 可以自由定义返回的 json 数据格式。

niuhe idl 中的更多关键字请参阅 niuhe 关键字

入门指南

本章节将介绍:

  • 实现 Hello World,
  • 如何生成 Swagger 文档和使用 Swagger 文档, 可导入 apifox, postman 等测试工具
  • MCP接入, 提升 AI 对文档的理解能力
  • 以及生成 TypeScript api, interface 定义。 可用于小程序, React, 等前端项目支持

Hello World

第二部分第二节种们通过定义 niuhe/all.niuhe 实现了一个简单的 Hello World 示例。本节我们将介绍如何实现它。 我们将 src/demo/app/api/views/hello_views.goWorld_GET 方法修改为如下:

func (v *Hello) World_GET(c *niuhe.Context, req *protos.HelloReq, rsp *protos.HelloRsp) error {
	rsp.Greeting = "Hello World " + req.Name
	return nil
}

重新运行程序并访问如下链接 http://localhost:19999/api/hello/world/?name=Tom,即可看到返回的问候语。

{
  "data": {
    "greeting": "Hello World Tom"
  },
  "result": 0
}

这里 github.com/ma-guo/niuhe 包提供了一些基础的功能,如上下文管理、请求参数解析等和返回结构包装。我们只关心具体业务逻辑即可。

插件生成 view 文件时会生成一个 views/XX.goviews/gen_XX.go, gen 开头的文件在每次生成代码时都会覆写, 请不要在其中修改代码。而 XX.go 则在文件存在时跳过写内容操作。

因此添加新的 api 定义时, 只需从 gen_XX.go 复制新添加的方法定义到 XX.go 中并去掉 gen_ 前缀即可。

生成 Swagger 文档

Swagger 文档是一种以特定格式(如 JSONYAML)编写的文件,用于对 RESTful API 进行详细的描述和说明。它提供了一种标准化的方式来定义 API 的端点、请求和响应的结构、参数、身份验证机制等信息,使得开发人员、测试人员、文档编写者以及其他相关人员能够全面了解 API 的功能和使用方法,从而更高效地进行开发、集成和测试等工作。

niuhe/.config.json5 文件 langs 配置中添加 docs 即可在生成文件时生成协议的 swagger 文档。

同时支持生成 openapi 3.0.1 格式的 openapi 文档, 在 langs 中添加 openapi 即可生成

生成的文档位于 docs/swagger.json, 同时生成了 niuhe/.docs.json5 自定义文件, 可在自定义文件中定制输出内容。

生成的 docs/swagger.json 文件可直接导入到 apifox 等支持 Swagger 协议的测试工具中进行测试和使用。也可读取 niuhe/.docs.json5 生成 url 输出到网页上。

示例

假设我们有一个简单的 API,包含以下内容:

with services():
    GET('协议文档', '/api/hello/docs/')

通过如下代码即可将协议文档输出到网页上:http://localhost:19999/api/hello/docs/

// 协议文档
func (v *Hello) Docs_GET(c *niuhe.Context) {
	docs, err := os.ReadFile("./docs/swagger.json")
	if err != nil {
		niuhe.LogError("read docs file failed: %v", err)
		return
	}
	data := make(map[string]any)
	json.Unmarshal(docs, &data)
	c.JSON(http.StatusOK, data)
}

线上示例:

生态后端管理系统项目 admin-core-niuhe 通过插件生成的 Swagger.json 文件,导入 Apifox 后的访问地址为:https://s.apifox.cn/aeac2eb5-398b-4bca-8ff7-3e7bfce5be79,点击即可查看接口文档。

生成 typescript api 定义

在开发前端应用时,通常需要根据后端的 API 接口定义来生成相应的 TypeScript 类型。niuhe 插件提供了便捷的方式来自动生成这些类型定义文件。以下是如何生成 TypeScript API 定义的步骤:

1. 添加 ts 语言支持

首先,在 docs/.config.json5langs 中添加 ts。生成代码后会生成如下几个文件:

typings
├── api.ts - API 接口类型定义
├── event.d.ts - 部分公共属性定义
├── request.ts - 请求方法封装
└── types.d.ts - niuhe 中定义的类型定义

2. 自定义 typescript 配置

上述生成的四个文件中 event.d.tsrequest.ts 和 内容是不会变的, 将其复制到目标位置即可。api.tstypes.d.ts 需要根据实际情况进行调整。因此在 docs/.config.json5 需要根据项目需要修改 typescript 配置。 typescript 定义如下:

interface TsConfig {
    app: string
    modes: string[]
    types: string[]
    api: string[]
    enumjs: string[]
    optional: boolean
    tsenum: boolean
}
interface EntryInfo {
    app: string // 项目名
    ...
    typescript: TsConfig[] // ts 相关配置, 在 langs 中添加 ts 时生效
    ...
}

参数说明:

  • app 生成 ts 代码中的 app 名, 不定义则保持同项目同名
  • modes 生成对应的 mode, 如有 api(前端网页项目)admin(后台管理项目) 两个 mode, 则可根据需要最小化生成代码。
  • types types.d.ts 存储路径, 支持相对路径和绝对路径, 支持多个
  • api api.ts 存储路径, 支持相对路径和绝对路径, 支持多个
  • enumjs js 版枚举类型定义, 需要在入口文件中引入, 如在 main.ts 中引入 import './enum.js'
  • optional niuhe 中定义的 optional 定义的参数生成代码时否生添加 ?, 默认值为 false
  • tsenum types.d.tsinterface 中字段为枚举时是否声明为对应枚举类型, 默认值为 false。当前是转为 int/string

引入 enumjs 配置项原因: 在 types.d.ts 中定义的 enum 在代码中引用时编辑器和编译器都不会报错。但是在运行时会报 is undefined 错误。此时需要在 window 中挂载对应枚举值即可修复, 引入此配置项即解决此问题。

其他实现方案

通过插件的 template 能力, 也可自行定义 typescript 形式, 具体参考模板库中 typescript enumtypesctipt apitypescript interface 三个例子。

niuhe 接入 MCP, 让 API 文档也智能

API 文档的未来:MCP,让协作像聊天一样简单. MCPModel Context Protocol(模型上下文协议)的缩写,是 2024 年 11 月 Claude 的公司 Anthropic 推出并开源的一个新标准。简单来说, 它就是让 AI 助手能够连接到各种第三方数据源的桥梁 ,包括你的内容库、业务工具和开发环境等等。说白了,就是让 AI 变得更聪明、回答更准确的一种方式!

利用 apifox-mcp-server 将在线文档、Swagger/OpenAPI 文件提供给 AI 使用, 从而让 AI 智能体能够更好地理解和使用你的 API, 从而提高工作效率。

niuhe 插件生成的 swagger.json 文档,能够完美借助 apifox-mcp-server 实现 MCP 功能。这意味着开发者可以轻松让 AI 智能体更深入、精准地理解和运用 API,大幅提升工作效率,让 API 开发与协作变得更加顺畅高效。

配置 MCP 客户端

前置条件

  • 已安装 Node.js 环境(版本号 >= 18,推荐最新的 LTS 版本)
  • 任意一个支持 MCP 的 IDE:
    • Cursor
    • VSCode + Cline 插件
    • 其它

基本步骤

  1. 确保你有 docs/swagger.json 本地路径或 url 地址

  2. 在 IDE 中配置 MCP

将下面 JSON 配置添加到 IDE 对应的 macOS/Linux MCP 配置文件如下:

{
  "mcpServers": {
    "API 文档": {
      "command": "npx",
      "args": [
        "-y",
        "apifox-mcp-server@latest",
        "--oas=<oas-url-or-path>"
      ]
    }
  }
}

windows MCP 配置文件如下:

{
  "mcpServers": {
    "API 文档": {
      "command": "cmd",
      "args": [
        "/c",
        "npx",
        "-y",
        "apifox-mcp-server@latest",
        "--oas=http://{your domain}/api/hello/docs/"
      ]
    }
  }
}

其中 <oas-url-or-path> 可以是:

  • 远程 URL,如:http://{your domain}/api/hello/docs/
  • 本地文件路径,如:~/Projects/docs/swagger.json

通过上述操作,你就可以在 IDE 中使用 MCP 连接到你的 API 文档了。如有其他疑问,请查阅下列参考链接或加官方支持群 咨询。

参考链接

进阶操作

本章节主要介绍一些高级功能和使用技巧,帮助你更好地利用 niuhe 进行开发。这些功能包括:

  • xorm 相关, 生成数据库表, 读写和 增删改查实现代码
  • 将 idl 定义内容转换为其他语言协议

以上功能可让您更聚焦核心功能开发, 拓展插件支持的编程语言, 并提升您的开发效率。

插件支持的语言

本插件中, 我们将生成不同格式代码文件特性定义为语言。

niuhe/.config.json5 文件中,langs 字段支持的可选项有 go,ts,docs,protocol, route, vitescript 7个值,其中 go 为默认支持的语言。下面对不同语言的特性进行说明

go

生成基础的 Go 代码,包括 API 接口定义, 路由管理, 基础数据库的 xorm 功能。在 niuhe 文件中进行定义后,开发时专注于具体逻辑开发即可,无需关心路由、请求参数解析等细节。

ts

参考 生成 typescript api 定义. 生成 TypeScript types 和 api 文件内容,interface 名称,字段名,字段说明, api 路由等同 niuhe 文件一一对应。生成代码后同 niuhe 文件保持同步。写前端代码时不用再对着协议文档手敲一份,减少了不必要的重复工作,同时避免了由于手动输入导致的错误。

docs

生成 Swagger 2.0 文档,方便 API 测试软件如 apifox(推荐), postman 等使用,在测试软件中通过文件/url 导入后即可直接进行 api 测试, 不需要再手动维护协议文档。生成的文档位于 docs/swagger.json

protocol

参考 生成其他协议, 生成简明扼要(同 swagger 而言)的 json 格式的协议文档, 方便开发者拓展为其他语言。生成的文档位于 docs/protocol.json

route

生成路由文件,方便开发者对 API 进行管理。如增加 api 级权限处理等。 生成的文档位于 src{appName}/app/{mode}/protos/gen_routes.go。在 错误代码定义和检测 章节中, 展示了routecodes 结合实现更丰富功能例子。

vite

参考 xorm 代码生成, 在配置 niuhe/.model.niuhe 文件后,并添加 vite 语言支持时会通过 .model.niuhe 定义生成 vue3-element-admin 项目 对应的前端 curd 页面. 生成的页面位于项目根目录 vite 文件夹下,需要手动将内容复制到 vue3-element-admin 项目具体 .vue 文件中

script

生成将项目发布服务时需要的脚本文件和服务器配置文件, 如 nginx.conf, supervisor.conf 等。可根据发布服务偏好选择是否添加 script 语言支持。

xorm 代码生成

在实际开发中, 每添加一个表时,一般就会有对应的增删改查操作, 而这些操作代码又高度相似,copy 代码时枯燥乏味。因此在插件中添加了 xorm 基本代码生成功能。让编码更聚焦核心功能。

如何使用

插件仅在 niuhe/.model.niuhe 中读取表定义并生成代码。

#alias 可选, 默认值为空, 用于指定表的别名
#mode 可选 默认值为 api,
#niuhe 可选, 默认值为 False, 在生成对应的 niuhe 是否覆盖已存在的 niuhe 文件内容
#dao 可选, 默认值为 False, 在生成对应的 dao 是否覆盖已存在的 dao 文件内容
#service 可选, 默认值为 False, 在生成对应的 service 是否覆盖已存在的 service 文件内容
#model 可选, 默认值为 False, 在生成对应的 model 是否覆盖已存在的 model 文件内容
#vite 可选, 默认值为 False, 在生成对应的 vite 是否覆盖已存在的 vite 文件内容, 需在 .config.json5 中 langs 中添加 "vite"
#这里是做示例用, 实际开发中直接写 class Config():即可
class Config(alias="系统配置", mode='api', niuhe=True, dao=True, service=True, model=True, vite=True):
    '''系统配置表'''
    name = required.String(desc='配置名称', index=True, search=True, len=255, notnull=True)# index 加索引, len varchar 最大长度, notnull 是否为空 search 分页查询时是否出现在参数中
    value = required.Long(desc='配置值', search=True)

上述参数说明

参数类型默认值必须描述
aliasstr''可选表的别名
modestr'api'可选生成代码的在哪个mode下
niuheboolFalse可选是否覆盖已存在的 niuhe 文件内容
daoboolFalse可选是否覆盖已存在的 dao 文件内容
serviceboolFalse可选是否覆盖已存在的 service 文件内容
modelboolFalse可选是否覆盖已存在的 model 文件内容
viteboolFalse可选是否覆盖已存在的 vite 文件内容, 需要配置 .config.json5 中的 langs 添加 "vite"

当首次生成代码时, 会生成以下几个文件(以 #app=demo)

  • niuhe/api_config.niuhe (需要手动 includeall.niuhe 中)
  • src/demo/xorm/models/config.go
  • src/demo/xorm/daos/config_dao.go
  • src/demo/xorm/services/config_svc.go
  • src/demo/app/api/views/config_views.go (include 后才会生成)
  • src/demo/app/api/views/gen_config_views.go (include 后才会生成)
  • vite/api_config.vue (需要在 .config.json5langs 添加 "vite")

除上述文件外, 如配置了 docs, ts 等语言的生成,也会更新对应的文件内容. 各个生成的文件内容如下:

本节以下文件内容均为插件生成

niuhe/api_config.niuhe


class ConfigItem():
	'''系统配置表'''
	id = optional.Long(desc='id')
	name = required.StringField(desc='配置名称')
	value = required.LongField(desc='配置值')
	create_at = optional.String(desc='创建时间')
	update_at = optional.String(desc='更新时间')

class ConfigFormReq():
	'''请求 Config 信息'''
	id = required.Long()

class ConfigPageReq():
	'''分页查询 Config 信息'''
	page = required.Integer(desc='页码')
	size = required.Integer(desc='每页数量')
	value = required.LongField(desc='配置值')

class ConfigPageRsp():
	'''分页查询 Config 信息'''
	total = required.Long(desc='总数')
	items = repeated.Message(cls=ConfigItem, desc='Config信息')

class ConfigDeleteReq():
	'''批量删除 Config 信息'''
	ids = repeated.Long(desc='记录id列表')

class ConfigNoneRsp():
	'''Config 无数据返回'''
	pass

with services():
	GET('分页查询获取 Config 信息', '/api/config/page/', ConfigPageReq, ConfigPageRsp)
	GET('查询获取 Config 信息', '/api/config/form/', ConfigFormReq, ConfigItem)
	POST('添加 Config 信息', '/api/config/add/', ConfigItem, ConfigItem)
	POST('更新 Config 信息', '/api/config/update/', ConfigItem, ConfigItem)
	DELETE('删除 Config 信息', '/api/config/delete/', ConfigDeleteReq, ConfigNoneRsp)

model 表定义

src/demo/xorm/models/config.go

package models

// Generated by niuhe.idl

// 如要同步表结构, 需要手动将 Config 手动添加到 models.go 文件的 GetSyncModels 数组中

import (
	"demo/app/api/protos"

	"time"
)

// 系统配置表
type Config struct {
	Id       int64     `xorm:"NOT NULL PK AUTOINCR INT(11)"`
	Name     string    `xorm:"VARCHAR(255) COMMENT('配置名称')"` // 配置名称
	Value    int64     `xorm:"INT COMMENT('配置值')"`           // 配置值
	CreateAt time.Time `xorm:"created"`                      // 创建时间
	UpdateAt time.Time `xorm:"updated"`                      // 更新时间
	DeleteAt time.Time `xorm:"deleted"`                      // 删除时间
}

func (row *Config) ToProto(item *protos.ConfigItem) *protos.ConfigItem {
	if item == nil {
		item = &protos.ConfigItem{}
	}
	item.Id = row.Id
	item.Name = row.Name
	item.Value = row.Value
	item.CreateAt = row.CreateAt.Format(time.DateTime)
	item.UpdateAt = row.UpdateAt.Format(time.DateTime)
	return item
}

dao 表定义

src/demo/xorm/daos/config_dao.go

package daos

// Generated by niuhe.idl

import (
	"demo/xorm/models"
	"github.com/ma-guo/niuhe"
)

// 系统配置表
type _ConfigDao struct {
	*Dao
}

// 系统配置表
func (dao *Dao) Config() *_ConfigDao {
	return &_ConfigDao{Dao: dao}
}

// 根据ID获取数据
func (dao *_ConfigDao) GetByIds(ids ...int64) ([]*models.Config, error) {
	rows := []*models.Config{}
	session := dao.db()
	err := session.In("id", ids).Desc("`id`").Find(&rows)
	if err != nil {
		niuhe.LogInfo("GetByIds Config error: %v", err)
		return nil, err
	}
	return rows, nil
}

// 分页获取数据
func (dao *_ConfigDao) GetPage(page, size int, value int64) ([]*models.Config, int64, error) {
	rows := make([]*models.Config, 0)
	session := dao.db()
	dao.Like(session, "`value`", value)
	dao.Limit(session, page, size)
	total, err := session.Desc("`id`").FindAndCount(&rows)
	if err != nil {
		niuhe.LogInfo("GetPage Config error: %v", err)
		return nil, 0, err
	}
	return rows, total, nil
}

service 表定义

src/demo/xorm/services/config_svc.go

package services

// Generated by niuhe.idl

import (
	"github.com/ma-guo/niuhe"

	"demo/xorm/models"
)

// 系统配置表
type _ConfigSvc struct {
	*_Svc
}

// 系统配置表
func (svc *_Svc) Config() *_ConfigSvc {
	return &_ConfigSvc{svc}
}

// 获取单个数据
func (svc *_ConfigSvc) GetById(id int64) (*models.Config, bool, error) {
	if id <= 0 {
		return nil, false, nil
	}
	row := &models.Config{Id: id}
	has, err := svc.dao().GetBy(row)
	if err != nil {
		niuhe.LogInfo("GetById Config error: %v", err)
	}
	return row, has, err
}

// 获取单个数据
func (svc *_ConfigSvc) GetBy(row *models.Config) (bool, error) {
	has, err := svc.dao().GetBy(row)
	if err != nil {
		niuhe.LogInfo("GetBy Config error: %v", err)
	}
	return has, err
}

// 更新数据
func (svc *_ConfigSvc) Update(row *models.Config) (bool, error) {
	has, err := svc.dao().Update(row.Id, row)
	if err != nil {
		niuhe.LogInfo("Update Config error: %v", err)
	}
	return has, err
}

// 插入数据
func (svc *_ConfigSvc) Insert(rows ...*models.Config) error {
	if len(rows) == 0 {
		return nil
	}
	// 2000条是经验值, 可根据自己需要更改
	batchSize := 2000
	for i := 0; i < len(rows); i += batchSize {
		end := i + batchSize
		if end > len(rows) {
			end = len(rows)
		}
		_, err := svc.dao().Insert(rows[i:end])
		if err != nil {
			niuhe.LogInfo("Insert Config error: %v", err)
			return err
		}
	}
	return nil
}

// 删除数据
func (svc *_ConfigSvc) Delete(rows []*models.Config) error {
	if len(rows) == 0 {
		return nil
	}
	_, err := svc.dao().Delete(rows)
	if err != nil {
		niuhe.LogInfo("Delete Config error: %v", err)
	}
	return err
}

// 根据 id 获取 map 数据
func (svc *_ConfigSvc) GetByIds(ids ...int64) (map[int64]*models.Config, error) {
	rowsMap := make(map[int64]*models.Config, 0)
	if len(ids) == 0 {
		return rowsMap, nil
	}
	rows, err := svc.dao().Config().GetByIds(ids...)
	if err != nil {
		niuhe.LogInfo("GetByIds Config error: %v", err)
		return nil, err
	}
	for _, row := range rows {
		rowsMap[row.Id] = row
	}
	return rowsMap, nil
}

// 分页获取数据
func (svc *_ConfigSvc) GetPage(page, size int, value int64) ([]*models.Config, int64, error) {
	rows, total, err := svc.dao().Config().GetPage(page, size, value)
	if err != nil {
		niuhe.LogInfo("GetPage Config error: %v", err)
	}
	return rows, total, nil
}

config_views 定义

src/demo/xorm/models/config_views.go

package services

// Generated by niuhe.idl

import (
	"github.com/ma-guo/niuhe"

	"demo/xorm/models"
)

// 系统配置表
type _ConfigSvc struct {
	*_Svc
}

// 系统配置表
func (svc *_Svc) Config() *_ConfigSvc {
	return &_ConfigSvc{svc}
}

// 获取单个数据
func (svc *_ConfigSvc) GetById(id int64) (*models.Config, bool, error) {
	if id <= 0 {
		return nil, false, nil
	}
	row := &models.Config{Id: id}
	has, err := svc.dao().GetBy(row)
	if err != nil {
		niuhe.LogInfo("GetById Config error: %v", err)
	}
	return row, has, err
}

// 获取单个数据
func (svc *_ConfigSvc) GetBy(row *models.Config) (bool, error) {
	has, err := svc.dao().GetBy(row)
	if err != nil {
		niuhe.LogInfo("GetBy Config error: %v", err)
	}
	return has, err
}

// 更新数据
func (svc *_ConfigSvc) Update(row *models.Config) (bool, error) {
	has, err := svc.dao().Update(row.Id, row)
	if err != nil {
		niuhe.LogInfo("Update Config error: %v", err)
	}
	return has, err
}

// 插入数据
func (svc *_ConfigSvc) Insert(rows ...*models.Config) error {
	if len(rows) == 0 {
		return nil
	}
	// 2000条是经验值, 可根据自己需要更改
	batchSize := 2000
	for i := 0; i < len(rows); i += batchSize {
		end := i + batchSize
		if end > len(rows) {
			end = len(rows)
		}
		_, err := svc.dao().Insert(rows[i:end])
		if err != nil {
			niuhe.LogInfo("Insert Config error: %v", err)
			return err
		}
	}
	return nil
}

// 删除数据
func (svc *_ConfigSvc) Delete(rows []*models.Config) error {
	if len(rows) == 0 {
		return nil
	}
	_, err := svc.dao().Delete(rows)
	if err != nil {
		niuhe.LogInfo("Delete Config error: %v", err)
	}
	return err
}

// 根据 id 获取 map 数据
func (svc *_ConfigSvc) GetByIds(ids ...int64) (map[int64]*models.Config, error) {
	rowsMap := make(map[int64]*models.Config, 0)
	if len(ids) == 0 {
		return rowsMap, nil
	}
	rows, err := svc.dao().Config().GetByIds(ids...)
	if err != nil {
		niuhe.LogInfo("GetByIds Config error: %v", err)
		return nil, err
	}
	for _, row := range rows {
		rowsMap[row.Id] = row
	}
	return rowsMap, nil
}

// 分页获取数据
func (svc *_ConfigSvc) GetPage(page, size int, value int64) ([]*models.Config, int64, error) {
	rows, total, err := svc.dao().Config().GetPage(page, size, value)
	if err != nil {
		niuhe.LogInfo("GetPage Config error: %v", err)
	}
	return rows, total, nil
}

新增的 protos 定义

src/demo/api/protos/gen_protos.go


// 系统配置表
type ConfigItem struct {
	Id       int64  `json:"id" zpf_name:"id"`                       //	id
	Name     string `json:"name" zpf_name:"name" zpf_reqd:"true"`   //	配置名称
	Value    int64  `json:"value" zpf_name:"value" zpf_reqd:"true"` //	配置值
	CreateAt string `json:"create_at" zpf_name:"create_at"`         //	创建时间
	UpdateAt string `json:"update_at" zpf_name:"update_at"`         //	更新时间
}

// 请求,Config,信息
type ConfigFormReq struct {
	Id int64 `json:"id" zpf_name:"id" zpf_reqd:"true"`
}

// 分页查询,Config,信息
type ConfigPageReq struct {
	Page  int   `json:"page" zpf_name:"page" zpf_reqd:"true"`   //	页码
	Size  int   `json:"size" zpf_name:"size" zpf_reqd:"true"`   //	每页数量
	Value int64 `json:"value" zpf_name:"value" zpf_reqd:"true"` //	配置值
}

// 分页查询,Config,信息
type ConfigPageRsp struct {
	Total int64         `json:"total" zpf_name:"total" zpf_reqd:"true"` //	总数
	Items []*ConfigItem `json:"items" zpf_name:"items"`                 //	Config信息
}

// 批量删除,Config,信息
type ConfigDeleteReq struct {
	Ids []int64 `json:"ids" zpf_name:"ids"` //	记录id列表
}

// Config,无数据返回
type ConfigNoneRsp struct {
}

vite 定义

文件路径 vite/api_config.vue。vite 需结合 vue3-element-admin 库使用, 是生成的增删改查 api 管理页面内容。

<template>
  <div class="app-container">
    <div class="search-container">
      <el-form ref="queryFormRef" :model="queryParams" :inline="true">
        <el-form-item prop="value" label="配置值">
          <el-input v-model="queryParams.value" placeholder="配置值" clearable @keyup.enter="fetchPage" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="fetchPage"><i-ep-search />搜索</el-button>
          <el-button @click="resetQuery"><i-ep-refresh />重置</el-button>
        </el-form-item>
      </el-form>
    </div>

    <el-card shadow="never" class="table-container">
      <template #header>
        <el-button @click="openDialogWithAdd()" type="success"><i-ep-plus />新增</el-button>
        <el-button type="danger" :disabled="state.ids.length === 0"
          @click="bantchDelete()"><i-ep-delete />删除</el-button>
      </template>
      <el-table ref="dataTableRef" v-loading="state.loading" :data="configItems" highlight-current-row border
        @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="ID" prop="id" align="center" />
        <el-table-column label="配置名称" prop="name" align="center" />
        <el-table-column label="配置值" prop="value" align="center" />
        <el-table-column fixed="right" label="操作" width="140" align="center">
          <template #default="{ row }">
            <el-button type="primary" size="small" link @click="openDialogWithEdit(row.id)">
              <i-ep-edit />编辑
            </el-button>
            <el-button type="primary" size="small" link @click="handleDelete(row.id)">
              <i-ep-delete />删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>

      <pagination v-if="state.total > 0" v-model:total="state.total" v-model:page="queryParams.page"
        v-model:limit="queryParams.size" @pagination="fetchPage" />
    </el-card>

    <!-- Config 表单弹窗 -->
    <el-dialog v-model="state.dialogVisible" :title="state.dialogTitle" @close="closeDialog">
      <el-form ref="configFormRef" :model="formData" :rules="rules" label-width="100px">
        <el-form-item label="ID" prop="id" v-if="formData.id > 0">
          <el-input v-model="formData.id" disabled placeholder="" />
        </el-form-item>
        <el-form-item prop="name" label="配置名称">
          <el-input v-model="formData.name" placeholder="配置名称" clearable />
        </el-form-item>
        <el-form-item prop="value" label="配置值">
          <el-input v-model="formData.value" placeholder="配置值" clearable />
        </el-form-item>
      </el-form>

      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确 定</el-button>
          <el-button @click="closeDialog">取 消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">

import { setConfigAdd } from "@/api/demo/api";
import { setConfigUpdate } from "@/api/demo/api";
import { deleteConfigDelete } from "@/api/demo/api";
import { getConfigForm } from "@/api/demo/api";
import { getConfigPage } from "@/api/demo/api";

defineOptions({
  name: "config",
  inheritAttrs: false,
});

const queryFormRef = ref(ElForm);

const configFormRef = ref(ElForm);
const configItems = ref<Demo.ConfigItem[]>();
const state = reactive({
  loading: false,
  total: 0,
  ids: [] as number[],
  dialogVisible: false,
  dialogTitle: "",
});

const queryParams = reactive<Demo.ConfigPageReq>({
  page: 1,
  size: 10,
  value: 0,
});

const formData = reactive<Demo.ConfigItem>({
  id: 0,
  name: "",
  value: 0,
  create_at: "",
  update_at: ""
});

// 根据需要添加校验规则
const rules = reactive({
  //   name: [{ required: true, message: "本字段必填", trigger: "blur" }],
});

/** 查询 */
const fetchPage = async () => {
  state.loading = true;
  const rsp = await getConfigPage(queryParams);
  state.loading = false;
  if (rsp.result == 0) {
    configItems.value = rsp.data.items;
    state.total = rsp.data.total;
  }
}
/** 重置查询 */
function resetQuery() {
  queryFormRef.value.resetFields();
  queryParams.page = 1;
  fetchPage();
}

/** 行checkbox 选中事件 */
function handleSelectionChange(selection: any) {
  state.ids = selection.map((item: any) => item.id);
}

/** 打开添加弹窗 */
function openDialogWithAdd() {
  state.dialogVisible = true;
  state.dialogTitle = "添加Config";
  resetForm();
}
/** 打开编辑弹窗 */
const openDialogWithEdit = async (roleId: number) => {
  state.dialogVisible = true;
  state.dialogTitle = "修改Config";
  state.loading = true;
  const rsp = await getConfigForm({ id: roleId });
  state.loading = false;
  if (rsp.result == 0) {
    Object.assign(formData, rsp.data);
  }
}

/** 保存提交 */
function handleSubmit() {
  configFormRef.value.validate((valid: any) => {
    if (valid) {
      if (formData.id) {
        updateRowRecord();
      } else {
        addRowRecord();
      }
    }
  });
}
/** 新增记录 */
const addRowRecord = async () => {
  state.loading = true;
  const rsp = await setConfigAdd(formData);
  state.loading = false
  if (rsp.result == 0) {
    ElMessage.success("添加成功");
    closeDialog();
    resetQuery();
  }
}
/** 修改记录 */
const updateRowRecord = async () => {
  state.loading = true;
  const rsp = await setConfigUpdate(formData);
  state.loading = false
  if (rsp.result == 0) {
    ElMessage.success("修改成功");
    closeDialog();
    resetQuery();
  }
}
/** 关闭表单弹窗 */
function closeDialog() {
  state.dialogVisible = false;
  resetForm();
}

/** 重置表单 */
function resetForm() {
  const value = configFormRef.value;
  if (value) {
    value.resetFields();
    value.clearValidate();
  }
  formData.id = 0;
  formData.name = "";
  formData.value = 0;
  formData.create_at = "";
  formData.update_at = "";
}

/** 删除 Config */
function handleDelete(id: number) {
  ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(async () => {
    state.loading = true;
    const rsp = await deleteConfigDelete({ ids: [id] });
    state.loading = false;
    if (rsp.result == 0) {
      ElMessage.success("删除成功");
      resetQuery();
    }
  });
}

/** 批量删除 */
const bantchDelete = () => {
  if (state.ids.length <= 0) {
    ElMessage.warning("请勾选删除项");
    return;
  }
  ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(async () => {
    state.loading = true;
    const rsp = await deleteConfigDelete({ ids: state.ids });
    state.loading = false;
    if (rsp.result == 0) {
      ElMessage.success("删除成功");
    }
    resetQuery();
  });
};

onMounted(() => {
  fetchPage();
});
</script>

生成其他协议

在支持 MCP 的AI编辑器下, 建议采用 MCP 功能 方式来生成协议。目前推荐使用下一节的方式: 用 template 生成其他语言协议 来生成协议。 template 方式中包含了大量参考例子template模板库供使用

在开发过程中,我们可能需要生成不同语言的协议文件。niuhe插件支持 go, ts, vite 等协议代码生成。同时为了方便其他语言使用。将 niuhe 协议以 json 格式输出了完整的协议内容,可以很方便地被其他语言解析和使用。以下是如何生成其他语言协议的步骤:

1. 配置 .config.json5

首先,在 .config.json5langs 中添加 protocol 配置项.

2. protocol 定义

其他语言协议实现若有疑问, 请加交流群 QQ [971024252]获取更多支持。 配置 protocol 后, 生成代码时会在 docs 文件下生成 protocol.json 文件。协议定义为:

declare namespace Protocol {
    interface StringEnum {
        value: string // 枚举值 - 数字也是 string
        name: string // 字段名
        desc: string // 描述
    }
    // 枚举类型
    interface Enum {
        mode: string
        type: 'string' | 'integer'
        name: string
        desc: string
        values: StringEnum[]
    }
    interface Field {
        label: string// required, optional, repeated, mapping
        type: string // 'integer', 'decimal', 'float', 'long', 'string', 'boolean', 'message', 'enum', 'stringenum','file' 'dict' 'any'
        name: string // 字段名
        desc: string //  描述
        ref?: string // 引用的结构
        deprecated?: boolean // 是否已经废弃
        version?: string // 版本号
    }
    interface Message {
        mode: string //api
        name: string // 名称
        desc: string // 描述
        fields: Field[]
    }
    interface Route {
        method: string // post, get[, put, patch, delete, head, options]
        url: string // /api/sys/ts/
        mode: string // api
		view: string // sys
		name: string // ts
        desc: string // 描述
        req?: Message
        rsp?: Message
        
    }
    interface Docs {
        app: string
        /** api 路由信息 */
        routes: Route[]
        /** 数组 */
        enums: {
            [mode_name: string]: Enum
        }
        /** 消息类型 */
        messages: {
            [mode_name: string]: Message
        }
    }
}

go 语言解析为 kotlin 版例子

通过解析上述协议文件可得到请求方法, 路径, 入参和出参的详细定义。下面为 go 语言解析出 kotlin 版本的一个例子。

本例子代码位于 src/demo/protocol/init.go

package protocol

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"unicode"

	"github.com/ma-guo/niuhe"
)

type StringEnum struct {
	Value string `json:"vallue"` // 枚举值 - 数字也是 string
	Name  string `json:"name"`   // 字段名
	Desc  string `json:"desc"`   // 描述
}

// 枚举类型
type Enum struct {
	Mode   string       `json:"mode"`
	Etype  string       `json:"type"` // 'string' | 'integer'
	Name   string       `json:"name"`
	Desc   string       `json:"desc"`
	Values []StringEnum `json:"values"`
}
type Field struct {
	Label string // required, optional, repeated, mapping
	Type  string `json:"type"` // 'integer', 'decimal', 'float', 'long', 'string', 'boolean', 'message', 'enum', 'stringenum','file' 'dict' 'any'
	Name  string `json:"name"` // 字段名
	Desc  string `json:"desc"` //  描述
	Ref   string `json:"ref"`  // 引用的结构
}
type Message struct {
	Mode   string   `json:"mode"` //api
	Name   string   `json:"name"` // 名称
	Desc   string   `json:"desc"` // 描述
	Fields []*Field `json:"fields"`
}
type Route struct {
	Method string   `json:"method"` // post, get[, put, patch, delete, head, options]
	Url    string   `json:"url"`    // /api/sys/ts/
	Mode   string   `json:"mode"`   // api
	View   string   `json:"view"`
	Name   string   `json:"name"`
	Desc   string   `json:"desc"` // 描述
	Req    *Message `json:"req"`
	Rsp    *Message `json:"rsp"`
}
type Docs struct {
	App string `json:"app"`
	/** api 路由信息 */
	Routes []Route `json:"routes"`
	/** 数组 */
	Enums map[string]*Enum `json:"enums"`
	/** 消息类型 */
	Messages map[string]*Message `json:"messages"`
}

// 解析结果
type ParseResult struct {
	Interfaces string
	Beans      string
}

func (doc *Docs) getKtType(meta *Field) string {
	switch meta.Type {
	case "integer":
		return "Int"
	case "decimal":
		return "Float"
	case "float":
		return "Float"
	case "long":
		return "Long"
	case "stringenum":
	case "string":
		return "String"
	case "boolean":
		return "Boolean"
	case "message":
		if meta.Ref != "" {
			return doc.fmtName(meta.Ref)
		}
		return doc.fmtName(meta.Ref)
	case "enum":
		return "Int"
	case "file":
		return "Any?"
	case "any":
		return "Any?"
	}
	return "Any?"
}
func (doc *Docs) getKtLabel(meta *Field) string {
	switch meta.Label {
	case "required":
		return doc.getKtType(meta)
	case "optional":
		return doc.getKtType(meta) + "?"
	case "repeated":
		return "List<" + doc.getKtType(meta) + ">?"
	case "mapping":
		return "Map<String, " + doc.getKtType(meta) + ">?"
	}
	return doc.getKtType(meta)
}
func getKtDefault(ntype string) string {
	switch ntype {
	case "integer":
		return "0"
	case "decimal":
		return "0f"
	case "float":
		return "0f"
	case "long":
		return "0"
	case "string":
		return "\"\""
	case "boolean":
		return "false"
	case "message":
		return "null"
	case "enum":
		return "0"
	case "file":
		return "null"
	case "any":
		return "null"
	}
	return "null"
}
func (doc *Docs) fmtName(name string) string {
	// 从 Hrhelper.StringEnum 返回 StringEnum
	if strings.Contains(name, ".") {
		return name[strings.Index(name, ".")+1:]
	}
	return name
}

// toCamelCase 函数用于将下划线分隔的字符串转换为驼峰命名法
func (doc *Docs) toCamelCase(s string) string {
	// 按下划线分割字符串
	parts := strings.Split(s, "_")
	var result strings.Builder

	for i, part := range parts {
		if i == 0 {
			// 第一个部分保持原样
			result.WriteString(part)
		} else {
			// 后续部分首字母大写
			for j, r := range part {
				if j == 0 {
					result.WriteRune(unicode.ToUpper(r))
				} else {
					result.WriteRune(r)
				}
			}
		}
	}

	return result.String()
}

func (doc *Docs) buildBean(item *Message) string {
	var lines strings.Builder
	lines.WriteString("@Serializable\n")
	lines.WriteString("data class " + doc.fmtName(item.Name) + "(\n")

	for _, field := range item.Fields {
		if field.Desc != "" {
			lines.WriteString("\n\t/** " + field.Desc + " */\n")
		}
		lines.WriteString("\t@SerialName(\"" + field.Name + "\")\n")
		lines.WriteString("\tval " + doc.toCamelCase(field.Name) + ": " + doc.getKtLabel(field) + " = " + getKtDefault(field.Type) + ",\n")
	}
	lines.WriteString(") {}\n")
	return lines.String()
}

func (doc *Docs) buildApi(route Route) string {
	var lines strings.Builder
	lines.WriteString("\t/**\n")
	if route.Desc != "" {
		lines.WriteString("\t * " + route.Desc + "\n\t *\n")
	}
	req := route.Req

	if len(req.Fields) > 0 {
		lines.WriteString("\t * 请求参数:\n")
		for _, value := range req.Fields {
			lines.WriteString("\t * @param " + doc.toCamelCase(value.Name) + " " + value.Desc + "\n")
		}
	}
	lines.WriteString("\t */\n")
	lines.WriteString("\t@FormUrlEncoded\n")
	lines.WriteString(fmt.Sprintf("\t@%v(\"%v\")\n", strings.ToUpper(route.Method), route.Url))
	rspRef := route.Rsp
	if len(req.Fields) > 0 {
		lines.WriteString("\tsuspend fun " + doc.fmtMethodName(route) + "(\n")
		for _, value := range req.Fields {
			lines.WriteString("\t\t@Field(\"" + value.Name + "\") " + doc.toCamelCase(value.Name) + ": " + doc.getKtLabel(value) + ",\n")
		}
		lines.WriteString("\t): CommRsp<" + doc.fmtName(rspRef.Name) + ">\n")
	} else {
		lines.WriteString("\tsuspend fun " + doc.fmtMethodName(route) + "(): CommRsp<" + doc.fmtName(rspRef.Name) + ">\n")
	}

	return lines.String()
}

// 将 /api/user/info/, post 转换为 postUserInfo
func (doc *Docs) fmtMethodName(route Route) string {
	method := strings.ToLower(route.Method)
	tmps := []string{method, route.View, route.Name}
	return doc.toCamelCase(strings.Join(tmps, "_"))
}

// 解析 protocol 文档
// docs 为 protocol.json 文件内容
func Parse(appName string, docs []byte) (*ParseResult, error) {
	info := &Docs{}
	err := json.Unmarshal(docs, info)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal protocol docs: %w", err)
	}
	var interfaces strings.Builder
	interfaces.WriteString("\nimport kotlinx.serialization.SerialName\n")
	interfaces.WriteString("interface " + appName + "{\n")

	for _, route := range info.Routes {
		interfaces.WriteString(info.buildApi(route))
	}
	interfaces.WriteString("}\n")
	result := &ParseResult{
		Interfaces: interfaces.String(),
	}

	var beans strings.Builder
	beans.WriteString("\nimport kotlinx.serialization.SerialName\n")
	beans.WriteString("import kotlinx.serialization.Serializable\n\n")
	for _, msg := range info.Messages {
		beans.WriteString(info.buildBean(msg))
	}
	result.Beans = beans.String()
	return result, nil

}

例子调用

调用例子如下:


func ShowKtProtocol(docPath string) {
	// docPath := "you protocol.json path"
	docs, err := os.ReadFile(docPath)
	if err != nil {
		niuhe.LogInfo("read docs error: %v", err)
		return
	}
	result, err := Parse("HrHelper", docs)
	if err != nil {
		niuhe.LogInfo("%v", err)
		return
	}
	// 保存到对应的文件
	niuhe.LogInfo("%v", result.Interfaces)
	niuhe.LogInfo("%v", result.Beans)

}

最佳实践

可结合 niuhe/.config.json5 中的配置 endcmd:[] 在生成代码后执行某个命令将上述解析的结果保存到对应位置获得更佳体验

下一节提供了更简易地用模板方法实现转换为其他目标语言的例子。

用 template 生成其他语言协议

在前一章节 生成其他语言协议 里,我们展示了如何解析 protocol 协议来生成目标语言的代码。在这一节,我们将演示如何通过 handlebars 语法 来实现生成其他语言代码的方法。

本节的演示代码位于项目 niuhedemo 中。更多模板库, 请移步template 模板库

开启 template 功能

niuhe/.config.json5 文件里,我们可以通过配置 templates 选项来开启模板渲染功能。下面是 templates 配置项的声明:

interface TplConfig {
    /** 模板类型, 可选为 message, enum, route */
    type: 'message' | 'enum' | 'route'
    modes: string[]
    /** template 文件地址 */
    template: string
    /** 存放的目标地址 */
    output: string
}

// 入口文件定义的信息
interface EntryInfo {
    ...
    /** 是否显示烟花效果 */
    fire?: boolean
    /** template 渲染 */
    templates?: TplConfig[]
}

生成代码

我们通过在 niuhe/.config.json5 中添加配置,来演示生成 typescript 脚本所需的类型、常量和 API 请求这三部分代码。其他语言的示例可以根据对应语言的特性,结合 handlebar 语法进行修改。

templates:[{
    type: 'message', // 可选值有 message, route, enum 三个, 同 protocol 协议相同
    template: './templates/ts_message.tpl', // 模板文件地址
    output: './output/message.d.ts', // 输出文件地址
    modes: ["api"], // 生成的 mode, url 第一部分
  },{
    type: 'route',
    template: './templates/ts_route.tpl',
    output: './output/route.ts',
    modes: ["api"],
  },{
    type: 'enum',
    template: './templates/ts_enum.tpl',
    output: './output/enum.d.ts',
    modes: ["api"],
  }]

生成类型定义代码

./templates/ts_message.tpl 代码如下:



// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改

interface Rsp<T> {
    result: number
    data: T
    message: string
}

declare namespace {{capitalize mod}} {
 {{#each items}}
 {{#if desc}}
    /** {{desc}} */
 {{/if}}
    interface {{name}} {
        {{#each fields}}
        {{#if desc}}
        /** {{desc}} */
        {{/if}}
        {{#if (isArray label)}}
        {{ json }}: {{ convertType type }}[],
        {{else}}
        {{ json }}: {{ convertType type }},
        {{/if}}
        {{/each}}
    }

{{/each}}
}

生成的 typescript 代码如下:



// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改

interface Rsp<T> {
    result: number
    data: T
    message: string
}

declare namespace Demo {
    /** 分页查询Config信息 */
    interface ConfigPageReq {
        /** 页码 */
        page: number,
        /** 每页数量 */
        size: number,
        /** 配置名称 */
        name: string,
        /** 配置值 */
        value: number,
    }

    /** 系统配置表 */
    interface ConfigItem {
        /** id */
        id: number,
        /** 配置名称 */
        name: string,
        /** 配置值 */
        value: number,
        /** 创建时间 */
        create_at: string,
        /** 更新时间 */
        update_at: string,
    }

    /** 分页查询Config信息 */
    interface ConfigPageRsp {
        /** 总数 */
        total: number,
        /** Config信息 */
        items: ConfigItem[],
    }

    /** 请求Config信息 */
    interface ConfigFormReq {
        id: number,
    }

    /** 批量删除Config信息 */
    interface ConfigDeleteReq {
        /** 记录id列表 */
        ids: number[],
    }

    /** Config无数据返回 */
    interface ConfigNoneRsp {
    }

    /** 测试请求 */
    interface HelloReq {
        /** 用户名 */
        name: string,
    }

    /** 测试响应 */
    interface HelloRsp {
        /** 问候语 */
        greeting: string,
    }

    /** 空请求 */
    interface NoneReq {
    }

    /** 空响应 */
    interface NoneRsp {
    }

    /** RPC测试用例请求参数 */
    interface XxxYyyReqMsg {
        /** 用户名 */
        name: string,
        /** 密码 */
        password: string,
    }

    /** RPC测试用例返回参数 */
    interface XxxYyyRspMsg {
        /** 用户open_id */
        open_id: string,
        /** 账户信息 */
        account_info: string,
    }

}

这里只展示了类型代码的生成,由于篇幅限制,常量和 API 代码请访问 示例项目查看

添加模板代码助手

在插件里,我们添加了 capitalizeisArraycameCase 这三个代码助手。在上一小节的演示中,用到了 capitalizeisArray,它们的定义如下:

// 首字母大写 Helper
handlebars.registerHelper('capitalize', (str: string): string => {
    if (!str) {
        return str;
    }
    if (str.length === 1) {
        return str.toUpperCase();
    }
    return str.charAt(0).toUpperCase() + str.slice(1);
});

// 数组 Helper
handlebars.registerHelper('isArray', (label: string): boolean => {
    return label === 'repeated';
});
// 驼峰命名 Helper
handlebars.registerHelper('cameCase', (text: string): string => {
    return camelCase(text);
});

添加自定义代码助手

当通过模板功能成功生成一次代码后, 会创建 niuhe/.template.tpl.ts 文件(这是自定义代码助手文件入口, 也可手动创建)。其默认内容如下:

// templates 注入 例子。 不能使用 js 高级语法。 
// 详细例子参考 https://handlebarsjs.com/zh/

handlebars.registerHelper('funName', function(args){
   return args;
});

为了在 template 里更简洁、灵活地编写模板代码,我们可以根据需求添加更多代码助手。

在上面的演示代码中,使用到了自定义的 convertTypeisInteger 两个代码助手,下面是演示代码中完整的 niuhe/.templates.tpl.js 内容:


// templates 注入 例子。 不能使用 js 高级语法。 
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/

handlebars.registerHelper('funName', function(args){
   return args;
});

handlebars.registerHelper('convertType', function (type) {
    const typeMap = {
        'string': 'string',
        'stringenum': 'string',
        'float': 'number',
        'double': 'number',
        'int': 'number',
        'long': 'number',
        'enum': 'number',
        'integer': 'number',
        'boolean': 'boolean',
        'object': 'any',
        'array': 'any[]',
        'map': 'any',
    };
    return typeMap[type] || type;
});

handlebars.registerHelper('isInteger', function (type) {
    return type === 'integer';
});

高效调试

在正确配置 niuhe/.config.json5 中的 templates 项后, 插件会在调用 handlebars 从模板文件编译目标文件前, 打印传入的数据内容(console.log({config, data}))。其中 data 是待编译的具体数据结构和数值。 我们可以通过 帮助 -> 切换开发人员工具 -> 控制台 页面查看打印的数据信息。 debug 截图

完整的数据结构定义

在编写 *.tpl 文件时, 数据项(message), 常量(enum)和请求方法(route) 定义结构如下。参考下面的定义,有助于我们高效、快速地编写模板代码。

declare namespace Tpl {
    /**
     * 字段定义属性, enum 比 message 多一个 value 字段
     */
    interface TplField {
        name: string;
        /** 字段描述 */
        desc?: string;
        /** 类型 专属字段 */
        type?: string;
        /** json 名 */
        json: string;
        /** 引用的结构 */
        ref?: TplMessage;

        /** 标签, required, optional, repeated, mapping */
        label: string;
        /** 字段版本 */
        version?: string;
        /** 是否废弃 */
        deprecated?: boolean

        /** enum 专属字段 */
        value?: string;
    }
    interface TplMessage {
        mode: string
        /* 类名 */
        name: string;
        /** 类描述 */
        desc?: string;
        /** 字段列表 */
        fields: TplField[];
        /** 是否废弃 */
        deprecated?: boolean;
        /** enum 专属字段, 为整数常量和字符串常量 */
        type?: 'integer' | 'string';
    }

    interface TplRoute {
        /** 如: api, url 第一段内容 */
        mode: string
        /** 路由方法, post, get, put, option... */
        method: string;
        /** api 路径 */
        url: string;
        /* 路由名 */
        name: string;
        /** 描述 */
        desc?: string;
        /** 返回结构 */
        rsp?: TplMessage;
        /** 请求结构 */
        req?: TplMessage;
        /** RPC定义返回的错误代码列表 */
        codes: TplField[]
    }
    interface Template {
        /** 数据项定义 */
        messages: TplMessage[]
        /** 常量定义 */
        enums: TplMessage[]
        /** 请求方法定义 */
        routes: TplRoute[]
    }
}

错误代码定义和检测

本功能需要 github.com/ma-guo/niuhe 版本 >=v1.0.5, niuhe 插件版本 >=0.6.20

当前插件支持通过 RPC().codes(...) 方式定义返回的错误代码列表。如果不进行额外处理, 在后端返回意料之外(非错误码列表中)的错误码时,后端无法感知到这一情形。为了支持这种检测, 我们需要再多做一些工作。

插件支持 .codes(LanguageType.ZH_CN, LanguageType.ZH_TW, Item(100, '错误描述')) 格式添加错误码。

codes 中包含Item 格式定义错误码时,将自动生成一个 ErrorEnumConstGroup 来定义错误码数据集, 此种方式更灵活。

利用下面的代码展示如何实现这一功能。

生成代码时, src/{mode}/app/views/init.go 文件内容如下:

package views

// Generated by niuhe.idl
import (
	"github.com/ma-guo/niuhe"
)

var thisModule *niuhe.Module

func GetModule() *niuhe.Module {
	if thisModule == nil {
		thisModule = niuhe.NewModule("${model}")
        // 需要在 .config.json5 中添加 langs: ["route"]
        // 使用下面的代码, 当错误码同 RPC 方式定义的 .codes(...) 不同时回收到回调
		// thisModule.AddRouteItem(protos.RouteItems...)
		// thisModule.RegisterCodeNotify(func(code int, path string) {
		// 	niuhe.LogError("code: %v with %v", code, path)
		// })
	}
	return thisModule
}
`

我们只需要在 .config.json5langs 中添加 "route", 然后将 GetModule 中被注释的三行代码打开即可快速实现检测这种非预期的错误码问题定位。

本功能优点

  • 无需修改业务代码, 即可实现错误码检测
  • 支持自定义错误码检测逻辑, 可主动发现意外错误码, 加快问题定位

endcmd 命令使用和讲解

niuhe/.config.json5 文件中预留了 endcmd:[] 配置项。这个配置允许你在生成代码后执行自定义的命令。以下是如何使用和讲解 endcmd 命令的步骤:

1. 命令详解

endcmd 为一字符串数组, 默认为空数组。一般第一个为命令名, 后续为参数, 如: go mod tidy 则定义为 ["go","mod","tidy"]。 切勿将所有命令写在一起, 否则可能执行失败。

配置文件完整说明

在项目首次生成代码时, 会生成 niuhe/.config.json5 配置文件, 配置文件内包含了常用的配置项, 少部分个性化的配置项并没有包含其中。这里介绍完整配置项和配置项目说明

配置项类型默认值说明
appstring""应用名, 定时时将覆盖 #app=xxx 定义的值。 示例值: "admin"
gomodstring""为空或未定义时同 app, 也可定义为其他名字, 如在 admin-core 项目中为 github.com/ma-guo/admin-core
langsstring[][]语言类型, "go"内定支持, 这里支持 "ts","docs","route","protocol", "vite", "openapi", "template"。示例值: ["ts", "docs"]。 参考插件支持的语言
tstypesstring[]["./typings/types.d.ts"]已废弃, langs 中支持 "ts" 时有效, 为生成的 ts 接口文件路径, 可定义多个, 如: tstypes:["full_types_path1", relative_types_path2"], 支持绝对路径和相对路径
tsapistring[]["./typings/api.ts"]已废弃, langs 中支持 "ts" 时有效, 为生成的 ts 接口文件路径, 可定义多个, 如: tsapi:`["full_api_path1", relative_api_path2"], 支持绝对路径和相对路径
typescriptTsConfig[][]langs中支持 ts 时有效, ts生成相关的配置文件, 参考生成 typescript api 定义
typescript.appstring"admin"ts中的 namespace 名字, 不定义时同应用名, 在需要将 namespcace 定义为其他值时使用。示例值: "admin"
typescript.modesstring[][]限定生成的 ts 中包含的 mode, 在大型项目中需要选择性生成最小化相关代码时使用。 示例值: ["api"]
typescript.typesstring[][./typings/types.d.ts]生成的 ts 接口文件路径, 可定义多个, 如: types:["full_types_path1", "relative_types_path2"], 支持绝对路径和相对路径
typescript.apistring[]["./typings/api.ts"]生成的 ts 接口文件路径, 可定义多个, 如: api:["full_api_path1", "relative_api_path2"], 支持绝对路径和相对路径
typescript.enumjsstring[][]生成的 js 枚举信息 文件路径, 可定义多个, 如: api:["full_enums_path1", "relative_enum2_path2"], 支持绝对路径和相对路径
typescript.tsenumboolfalseniuhe中定义的字段 typeEnum/StringEnum 时在 types 中是生成为对应的 group 定义, 为 false 时转换为 int/string,示例值:false
typescript.optionalboolfalseniuhe中定义的字段 labeloptional 时在 types 中是否要添加 ?,示例值:false
viteViteConfig[][]langs中支持 vite 时有效, 前端页面生成相关的配置文件, 建议保持同 typescript中同名配置保持一致。 参考 xorm 代码生成
vite.appstring""ts代码中的引用的 namespace 名字, 不定义时同应用名。 建议同 typescript.app保持一致。示例值:"admin"
vite.tsenumboolfalseniuhe中定义的字段 type 为 Enum/StringEnum 时在 types 中是生成为对应的 group 定义, 为 false 时转换为 int/string
showlogboolfalse生成代码时是否生成日志, 打开时,日志在项目目录下 niuhe.log 中, 生成错误时可进行排查,示例值:"false"
endcmdstring[][]生成代码后执行的命令, 默认为空, 一般第一个为命令名, 后续为参数, 如: go mod tidy 则定义为 ["go","mod","tidy"]
fireboolfalse烟花效果,示例值:false
templatesTplConfig[][]langs中支持template时有效,使用模板方式生成其他语言时自定义配置, 参考 用 template 生成其他语言协议
templates.typestring""可选值为"message","enum""route", 分别对应消息,枚举和 api。示例值:"message"
templates.modesstring[][]限定生成的模板中包含的 mode, 在大型项目中需要选择性生成最小化相关代码时使用。示例值:["api"]
templates.templatestring""模板文件地址, 支持绝对和相对路径。示例值:"./templates/kt_enum.tpl"
templates.outputstring""模板生成代码后的保存地址, 支持绝对和相对路径。示例值:"./output/Enums.kt"
escapeboolfalse生成 templates 时是否转义, 默认不转义,示例值:false
timerboolfalse打印本次生成代码各部分耗时日志, showlog 为 true 时打印在日志最后, 同时可在 帮助->切换开发人员工具->控制台中查看。示例值:false
viewprefixstring""生成的文件前缀, 同一个 view 会生成 ${view}_views.gogen_${view}_views.go 两个文件, 存在多个 view 时会排序杂乱, 统一添加前缀便于将${view}_views.gogen_* 文件分离出来。特别地, 当 viewprefix 为 true/"true" 时, 其值同 mode

配置项定义

// 生成 typescript 相关配置
interface TsConfig {
    app: string
    modes: string[]
    types: string[]
    api: string[]
    /** 枚举的 js 文件名 */
    enumjs: string[]
    tsenum: boolean
    optional: boolean

}
// 通过 .model.niuhe 生成前端代码配置
interface ViteConfig {
    app: string
    /** field 为 enum 的是否需要转换类型 */
    tsenum: boolean
}
// templates 相关配置
interface TplConfig {
    /** 模板类型, 可选为 message, enum, route */
    type: 'message' | 'enum' | 'route'
    modes: string[]
    /** template 文件地址 */
    template: string
    /** 存放的目标地址 */
    output: string

// 配置文件定义
interface EntryInfo {
    /** 项目名, 定义时覆盖: `#app=admin` */
    app: string // 
    /** go module 名字, 默认同 app 保持一直 */
    gomod: string
    /** 支持的语言, 默认支持 `go`, 可选为 `ts`, `docs`, `openapi`, `route`, 'protocol', 'vite, 以 逗号分割 */
    langs: string[] //
    /** @deprecated ts types.d.ts 自定义的地址 */
    tstypes: string[]
    /** @deprecated ts api.ts 自定义的地址 */
    tsapi: string[]
    typescript: TsConfig[]
    vite: ViteConfig
    /** 是否支持生成日志, 默认为 false */
    showlog: boolean
    /** 结束后执行的命令, 可选 */
    endcmd: string[]
    /** 是否显示烟花效果 */
    fire?: boolean
    /** render 模板 */
    templates?: TplConfig[]
    /** 生成 template 时是否转义, 默认不转义 */
    escape?: boolean
    /** 打印耗时 */
    timer?: boolean
    /** ${view}_views.go 文件前缀 */
    viewprefix?: string
}

配套管理后台

本章节主要介绍如何使用 niuhe 插件生成配套的管理后台 admin-core和前端项目vue3-element-admin 的使用

admin-core是一款开箱即用的中后台解决方案,它集成了RBAC权限体系,快速实现用户/角色/菜单管理, 文件存储等基本功能, 在开发时几行代码接入即可实现功能集成。同时支持自定义扩展功能。

vue3-element-admin 是一个基于Vue3和Element Plus的完整后台功能组件,提供丰富的UI组件和布局方案,帮助开发者快速构建企业级后台应用。

本模板提供了在线演示(账号: admin / 123456), 点击链接可预览模板。

插件 xorm 代码生成 功能支持生成 vue3-element-admin 页面代码, 基于 xorm 的数据库操作代码等,实现增删改查功能。实现管理后台从后端到前端的基本实现,加快功能开发速度。

管理后台支持的功能清单如下:

功能模块支持情况备注
用户管理-
菜单管理-
角色管理-
部门管理-
岗位管理-
接口管理API级权限控制
日志管理-
字典管理-
OSX针对各大云厂商云存储文件上传服务,根据系统配置前端自动上传
文件管理管理上传的文件
内容管理文章内容管理

希望让你早点下班,多点时间陪伴家人

免责声明

该方案提供的是“现况”,没有任何保证,表示或暗示,包括但不限于担保的质量,性能,不侵权,适销性或特定用途的适用性。在任何情况对于任何因使用本方案包装所产生的 任何直接性、间接性、偶发性、特殊性、惩罚性或任何结果的损害(包括但不限于替代商品或劳务之购用、使用损失、资料损失、利益损失、业务中断等等),admin-core 不负任何责任。不允许使用该框架做一切违反中华人民共和国法律的事情(包含软件但不限于软件)。

服务端 admin-core 引入

admin-core 是管理后台项目 vue3-element-admi 的后端支持项目。本身也是 niuhe 插件生成的项目. 可在项目中通过简单的设置来接入它

1. main 中引入

demo 项目为例, 当前 src/demo/main.go 内容为:

package main

// Generated by niuhe.idl

import (
	apiViews "demo/app/api/views"
	"demo/config"
	"fmt"
	"github.com/ma-guo/niuhe"
	"os"
)

type baseBoot struct{}

type BaseBoot struct {
	baseBoot
}

func (baseBoot) LoadConfig() error {
	if len(os.Args) < 2 {
		return fmt.Errorf("Usage: %s <config-path>", os.Args[0])
	}
	return config.LoadConfig(os.Args[1])
}

func (baseBoot) BeforeBoot(svr *niuhe.Server) {}

func (baseBoot) RegisterModules(svr *niuhe.Server) {
	svr.RegisterModule(apiViews.GetModule())

}

func (baseBoot) Serve(svr *niuhe.Server) {
	svr.Serve(config.Config.ServerAddr)
}

func main() {
	boot := BaseBoot{}
	if err := boot.LoadConfig(); err != nil {
		panic(err)
	}
	svr := niuhe.NewServer()
	boot.BeforeBoot(svr)
	boot.RegisterModules(svr)
	boot.Serve(svr)
}

接入步骤:

  • 在 import 中添加依赖 adminBoot "github.com/ma-guo/admin-core/boot"
  • 加载 admin-core 配置文件
  • 修改 RegisterModules 方法, 注册 admin-core 模块 接入后代码如下
package main

// Generated by niuhe.idl

import (
	apiViews "demo/app/api/views"
	"demo/config"
	"fmt"
	"os"

	adminBoot "github.com/ma-guo/admin-core/boot" // 第一步
	"github.com/ma-guo/niuhe"
)

type baseBoot struct{}

type BaseBoot struct {
	baseBoot
}

func (baseBoot) LoadConfig() error {
	if len(os.Args) < 2 {
		return fmt.Errorf("Usage: %s <config-path>", os.Args[0])
	}
	return config.LoadConfig(os.Args[1])
}

func (baseBoot) BeforeBoot(svr *niuhe.Server) {}

func (baseBoot) RegisterModules(svr *niuhe.Server) {
	svr.RegisterModule(apiViews.GetModule())

}

func (baseBoot) Serve(svr *niuhe.Server) {
	svr.Serve(config.Config.ServerAddr)
}

func main() {
	boot := BaseBoot{}
	if err := boot.LoadConfig(); err != nil {
		panic(err)
	}
    // 第二步, 加载配置文件
	admin := adminBoot.AdminBoot{}
	if err := admin.LoadConfig(os.Args[1]); err != nil {
		niuhe.LogInfo("admin config error: %v", err)
		return
	}

	svr := niuhe.NewServer()
	boot.BeforeBoot(svr)
    // 第三步, 注册 admin-core 模块
	admin.RegisterModules(svr)
	boot.RegisterModules(svr)
	boot.Serve(svr)
}

其他

接入后续在 src/demo 目录下运行 go mod tidygo mod vendor 来下载依赖

登录验证 - 可选

引入 admin-core 后, 本项目的 api 是不会进行登录验证的, 如果需要登录验证, 还需要一些操作才行。添加认证步骤如下:

  • 自定义方法需要加入 Bearea 认证的请参考 src/admincoretest/views/init.go 中的使用
  • 需要修改 Bearea 认证盐请在配置文件中配置 secretkey 值

加入 Bearear 认证

src/demo/app/api/views/init.go 中,现代码如下:

package views

// Generated by niuhe.idl
import (
	"github.com/ma-guo/niuhe"
)

var thisModule *niuhe.Module

func GetModule() *niuhe.Module {
	if thisModule == nil {
		thisModule = niuhe.NewModule("api")
	}
	return thisModule
}

将代码修改如下:

package views

// Generated by niuhe.idl
import (
	coreViews "github.com/ma-guo/admin-core/app/v1/views" // 引入 admin-core 模块
	"github.com/ma-guo/niuhe"
)

var thisModule *niuhe.Module

func GetModule() *niuhe.Module {
	if thisModule == nil {
		// thisModule = niuhe.NewModule("api")
		coreViews.AddSkipUrl("/api/hellow/docs/") // 不需要认证的路径都加入到这里来
		thisModule = niuhe.NewModuleWithProtocolFactoryFunc("api", func() niuhe.IApiProtocol {
			return coreViews.GetProtocol() // 使用 coreViews 中定义的协议处理
		})
	}
	return thisModule
}

总结

通过在 main.go 和 views/init.go 文件中的简单修改,即可将 admin-core 引入到项目中。

使用 API 级权限校验 - 可选

API 级校验接入后需要在后台进行配置对应权限, 同时服务端也需要进行一些简单接入。路由信息在运行 niuhe 插件生成的 API 后会自动添加到数据表中。使用权限校验需要两步处理

在 langs 中添加 route

src/niuhe/.config.json5 文件中的 langs 字段下添加路由信息 "route"。配置后会在 src/{app}/app/api/protos/gen_routes.go 中生成项目定义的路由信息。

将路由信息接入加入到 protocol 中

通过以下几行代码, 可将路由信息接入到 protocol 中。

routes := []*coreProtos.RouteItem{}
for _, route := range protos.RouteItems {
        routes = append(routes, &coreProtos.RouteItem{
                Method: route.Method,
                Path:   route.Path,
                Name:   route.Name,
        })
}
coreViews.GetProtocol().AddRoute("", routes)

接着上一节的内容, 结果 api 校验后的 views/init.go 文件内容如下:

package views

// Generated by niuhe.idl
import (
	"demo/app/api/protos"

	coreProtos "github.com/ma-guo/admin-core/app/v1/protos"
	coreViews "github.com/ma-guo/admin-core/app/v1/views"
	"github.com/ma-guo/niuhe"
)

var thisModule *niuhe.Module

func GetModule() *niuhe.Module {
	if thisModule == nil {
		// thisModule = niuhe.NewModule("api")
		// api 级权限处理
		routes := []*coreProtos.RouteItem{}
		for _, route := range protos.RouteItems {
			routes = append(routes, &coreProtos.RouteItem{
				Method: route.Method,
				Path:   route.Path,
				Name:   route.Name,
			})
		}
		coreViews.GetProtocol().AddRoute("", routes)

		coreViews.AddSkipUrl("/api/hellow/docs/") // 不需要认证的路径都加入到这里来
		thisModule = niuhe.NewModuleWithProtocolFactoryFunc("api", func() niuhe.IApiProtocol {
			return coreViews.GetProtocol() // 使用 coreViews 中定义的协议处理
		})
	}
	return thisModule
}

vue3-element-admin 项目搭建指南

简介

vue3-element-admin 是基于 Vue3 + Vite5+ TypeScript5 + Element-Plus + Pinia 等主流技术栈构建的免费开源的后台管理前端模板(配套后端源码 admin-core

项目启动

# 克隆代码
git clone https://github.com/ma-guo/vue3-element-admin.git

# 切换目录
cd vue3-element-admin

# 安装 pnpm
npm install pnpm -g

# 安装依赖
pnpm install

# 启动运行
pnpm run dev

生成新页面代码

niuhe/.config.json5 配置项 langs 中添加 vite, 结合 niuhe/.model.niuhe 配置, 在生成代码时会在 vite 目录下生成对应的 vue 文件。 将生成的文件内容复制到对应的 vue 文件中即可

自定义协议处理

本插件生成的入参和出参处理定义在 niuhe 库中, 具体代码为:


type IApiProtocol interface {
	Read(*Context, reflect.Value) error
	Write(*Context, reflect.Value, error) error
}

type DefaultApiProtocol struct{}

func (self DefaultApiProtocol) Read(c *Context, reqValue reflect.Value) error {
	if err := zpform.ReadReflectedStructForm(c.Request, reqValue); err != nil {
		return NewCommError(-1, err.Error())
	}
	return nil
}

func (self DefaultApiProtocol) Write(c *Context, rsp reflect.Value, err error) error {
    if c._ignoreResult {
		return nil
	}
	rspInst := rsp.Interface()
	if _, ok := rspInst.(isCustomRoot); ok {
		c.JSON(200, rspInst)
	} else {
		var response map[string]interface{}
		if err != nil {
			if commErr, ok := err.(ICommError); ok {
				response = map[string]interface{}{
					"result":  commErr.GetCode(),
					"message": commErr.GetMessage(),
				}
				if commErr.GetCode() == 0 {
					response["data"] = rsp.Interface()
				}
			} else {
				response = map[string]interface{}{
					"result":  -1,
					"message": err.Error(),
				}
			}
		} else {
			response = map[string]interface{}{
				"result": 0,
				"data":   rspInst,
			}
		}
		c.JSON(200, response)
	}
	return nil
}

如果需要自行实现为其他协议, 可以参考上述代码进行自定义实现。

admin-core FAQ

admin-core 是一个基于 Vue3 + Element Plus 的后端管理系统模板,它提供了一套完整的后台解决方案。在使用过程中,可能会遇到一些常见问题。以下是一些常见问题的解答和解决方案。

首次登录密码问题

登录失败时会将请求的密码生成的加密字符串 log 出来,首次登录时将字符串替换到表中对应用户的密码字段即可。

OSS 路径问题

admin-core 中添加了文件存储功能, 默认为本地存储,本地存储需要在配置文件中添加 host 字段 和 fileprefix 字段, 当前 conf/demo.yaml 内容为:

serveraddr: :19999
loglevel: INFO
db:
  showsql: false
  debug: true
  sync: true
  main: user:pwd@tcp(host:port)/database_name?charset=utf8mb4
secretkey: 123456
fileprefix: xxx
host: http://localhost:19999

fileprefix 文件存储的前缀, secretkeybear 登录加密的 salt

总结

通过以上步骤,您可以将 admin-core 引入到您的项目中,并根据实际需求进行定制化开发。希望这些信息对您有所帮助!

致谢

生成的 go 代码路由管理项目修改至 github.com/zp-server/niuhe, 在原库的基础上进行了微小修改以支持除 GET, POST 外的其他 http 方法。 入参的读取库 fork 至 github.com/ziipin-server/zpform, 而管理后台前端项目则 fork 至 https://github.com/youlaitech 。感谢这些库的作者和贡献者的无私贡献和开源精神。

相关代码库

核心库

演示代码

生态系统

FAQ

感谢使用者, 希望在使用中提出宝贵的反馈和改进建议,下面二维码是我们的联系方式,期待您的加入。 交流群 QQ [971024252] 点击链接加入群聊【niuhe插件支持】 971024252

niuhe IDL 语言关键字

应用相关

语法: #app=demo

关键字说明
app通过 #app=demo 给应用命名

文件相关

语法: include('api_user.niuhe')

关键字说明
include通过 include 引入定义的其他文件

常量组 ConstGroup 相关

语法: class Lang(ConstGroup[,desc='', depr=True,ver='1.1'])

关键字说明关键字说明
class常量组开始标记ConstGroup将结构标记为常量组
desc常量组描述depr常量组标记为废弃
deprecated同 depr 将常量组标记为废弃ver声明版本

常量相关

语法 ZH_CN=Item(1, "中文")ZH_CN=Item(value=1, desc="中文", depr=True, ver='1.1')

关键字说明关键字说明
Item常量标记ConstGroup将结构标记为常量组
value指定常量的值desc常量组描述
depr常量组标记为废弃deprecateddepr 将常量组标记为废弃
ver声明版本name未定义常量名时重命名

类相关

语法: class User([desc='用户', depr=True, ver='1.1', used=True]):

关键字说明关键字说明
class类开始标记Message将结构标记为类,可不写
desc常量组描述, 也可写为下一行的注释depr常量组标记为废弃
deprecateddepr 将常量组标记为废弃ver声明版本
used将类标记为已被引用,确保在代码中会生成本类相关代码

类字段相关

语法: name=label.type(attrs) 例子: avatar = optional.String(desc='头像', depr=True, ver='1.1')

类字段 label 部分

关键字说明关键字说明
required作为请求参数时本参数必填optional作为请求结构时本参数可选
repeated表示生成的成员为数组mapping表示生成的成员为 map 形式, 内部可填充任意值

类字段 type 部分

关键字说明关键字说明
Integer字段类型为 intLong字段类型为 int64
Float字段类型为 float64Decimal字段类型为 float64
String字段类型为 stringBoolean字段类型为 boolean
Enum字段类型为整数常量StringEnum字段类型为字符串常量
Message字段类型为类File字段类型文件, 读取 header 中对应的文件
Any字段类型map[string]any

类字段 attrs 部分

关键字说明关键字说明
desc字段描述depr常量组标记为废弃
deprecateddepr 将常量组标记为废弃ver声明版本
json重命令json 名omit值为空序列化时丢弃本字段
demo字段示例值cls类型为Message时指定具体的类
group类型为 Enum/StringEnum 时指定常量组枚举regtypeString 时验证其正则表达式
minlentypeString 时验证其最小长度maxlentypeString 时验证其最小长度
minnumtype 数字时验证其最小值maxnumtype 数字时验证其最大值
format指定其对应结构的 Parse方法格式,如 format="2006-01-02"

attrsreg, minlen, maxlen, minnum, maxnum 的属性请参考 zpform struct 定义

路由相关

示例

with services():
    GET('某天数据', '/api/stat/day/',StatDayReq, StatDayRsp, author='xxx', status=tested)
    RPC('统计充值情况', get='/api/stat/charge/',author='xxx', status=tested).args(
        team_id = optional.Long(desc='id'),
    ).returns(
        total = required.Message(cls=StatChargeItem, desc='总充值数据'),
    ).codes(
        comm.LangEnum.ZH_CN, Item(100, '权限错误'),Item(101),
    )
关键字说明关键字说明
with路由开始标记services路由开始标记
POSThttp 请求为 POSTGEThttp 请求为GET
PUThttp 请求为PUTPATCHhttp 请求为PATCH
DELETEhttp 请求为DELETEHEADhttp 请求为HEAD
OPTIONShttp 请求为OPTIONSRPCRPC 方式定义路由
author指定接口责任人/作者status指定接口状态

RPC 定义相关

关键字说明关键字说明
url指定方法为GETPOSTargs定义请求参数
returns定义返回参数codes定义约束错误码集合

XORM 代码生成相关

代码示例

class Config(alias="系统配置", mode='api', niuhe=True, dao=True, service=True, model=True, vite=True):
    '''系统配置表'''
    name = required.String(desc='配置名称', index=True, search=True, len=255, notnull=True)

xorm 表相关

关键字说明关键字说明
class开始标记alias表文字说明
mode定义 CURD url 中的 mode 字段niuhe在生成对应的 niuhe 是否覆盖已存在的 niuhe 文件内容
dao在生成对应的 dao 是否覆盖已存在的 dao 文件内容service在生成对应的 service 是否覆盖已存在的 service 文件内容
model在生成对应的 model 是否覆盖已存在的 model 文件内容vite在生成对应的 vite 是否覆盖已存在的 vite 文件内容, 需在 .config.json5 中 langs 中添加 "vite"`

xorm 字段相关

语法: name=label.type(attrs)

labeltype 部分同类中的字段, 这里仅列出 attrs 部分

关键字说明关键字说明
desc字段描述group类型为 Enum/StringEnum 时指定常量组枚举
json重命令json 名omit值为空序列化时丢弃本字段
demo字段示例值ver字段版本号
index将字段标记为索引len指定字段长度
notnull字段标记为非空date字段类型为 time.Time
default指定字段默认值search字段是否出现在分页搜索参数中

template 模板库

用 template 生成其他语言协议 章节介绍了用模板生成目标语言的方法。为了方便大家使用、提高效率,这里收集了一些语言的模板库。使用时,大家可以根据自己的需求稍作修改就能用。

如果您有想分享的模板,希望收录到这里,可以加入 QQ 交流群:971024252 联系我们。

写模板文件时, Vscode 内联为 HTML 语言, 可获得更佳编码体验

模板列表

生成 Kotlin service 例子

.templates.tpl.js 中添加 handlebars 自定义钩子

// templates 注入 例子。 不能使用 js 高级语法。
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/

handlebars.registerHelper("convertKtType", function (type) {
  const typeMap = {
    string: "String",
    stringenum: "String",
    number: "Int",
    float: "Float",
    double: "Double",
    int: "Int",
    long: "Long",
    enum: "Int",
    integer: "Int",
    boolean: "Boolean",
    object: "Any",
    array: "List<Any>",
    map: "Map<String, Any>",
  };
  return typeMap[type] || type || "--";
});

// 大写
handlebars.registerHelper("upperCase", function (args) {
  if(!args) {
    return args;
  }
  return args.toUpperCase();
});

handlebars.registerHelper("formType", function (method) {
  if(method === "POST" || method === "post") {
    return "@Field";
  }
  return "@Query";
});

handlebars.registerHelper("shouldUrlEncode", function (method) {
  if(method === "POST" || method === "post") {
    return this.req && this.req.fields.length>0;
  }
  return false;
});

template 编写

package your_package

import your_package.model.*
import retrofit2.Response
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
import retrofit2.http.GET
import retrofit2.http.Query

{{!-- 模板使用到的自定义钩子: convertKtType, shouldEncode, formType, upperCase }} --}}
interface ApiService {
 {{#each items}}

{{#if desc}}
    /** {{desc}} */
{{/if}}
{{#if (shouldUrlEncode method)}}
    @FormUrlEncoded
{{/if}}
    @{{upperCase method}}("{{url}}")
    suspend fun {{name}}(
        {{#each req.fields}}
        {{#if desc}}
        /** {{desc}} */
        {{/if}}
        {{#if (isArray label)}}
        {{formType ../method}}("{{ json }}") {{ name }}: List<{{ convertKtType type }}>,
        {{else}}
        {{formType ../method}}("{{ json }}") {{ name }}: {{ convertKtType type }},
        {{/if}}
        {{/each}}
    ): Response<Rsp<{{rsp.name}}>>
{{/each}}
}

.config.json5 参考配置

 {
    templates: [{
      modes: ["api"],
      template: "./templates/kt_route.tpl",
      type: "route",
      output: "./output/ApiService.kt",
    }]
 }

生成 Kotlin model 例子

.templates.tpl.js 中添加 handlebars 自定义钩子

// templates 注入 例子。 不能使用 js 高级语法。
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/

handlebars.registerHelper("convertKtType", function (type) {
  const typeMap = {
    string: "String",
    stringenum: "String",
    number: "Int",
    float: "Float",
    double: "Double",
    int: "Int",
    long: "Long",
    enum: "Int",
    integer: "Int",
    boolean: "Boolean",
    object: "Any",
    array: "List<Any>",
    map: "Map<String, Any>",
  };
  return typeMap[type] || type || "--";
});

handlebars.registerHelper("ktClass", function (fields) {
  if (!fields || fields.length == 0) {
    return "class";
  }
  return "data class";
});

handlebars.registerHelper("shouldSerial", function () {
  return this.name !== this.json
});

template 内容

model.tpl 需要生成请求和返回以及引用到的文件定义。 you_package 需修改为你的目标包名

package your_package

import kotlinx.serialization.SerialName
import com.google.gson.annotations.SerializedName

{{!-- 模板使用到的自定义钩子: ktClass, convertKtType, isArray(内置),shouldSerial }} --}}

data class Rsp<T>(
    val result: Int,
    val data: T?,
    val message: String?,
) {
    /** 请求成功 */
    fun isSuccess(): Boolean {
        return result == 0
    }

}
 {{#each items}}

 {{#if desc}}
/** {{desc}} */
{{/if}}
{{ktClass fields}} {{name}}(
    {{#each fields}}
    {{#if desc}}
    /** {{desc}} */
    {{/if}}
    {{#if (shouldSerial)}}
    @SerializedName("{{ json }}")
    @SerialName("{{ json }}")
    {{/if}}
    {{#if (isArray label)}}
    val {{ name }}: List<{{ convertKtType type }}>,
    {{else}}
    val {{ name }}: {{ convertKtType type }},
    {{/if}}
    {{/each}}
)
{{/each}}

.config.json5 参考配置

 {
    templates: [{
      modes: ["api"],
      template: "./templates/kt_message.tpl",
      type: "message",
      output: "./output/Models.kt",
    }]
 }

生成 Kotlin enum 例子

.templates.tpl.js 中添加 handlebars 自定义钩子

// templates 注入 例子。 不能使用 js 高级语法。
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/

handlebars.registerHelper("isInteger", function () {
  return this.type === "integer";
});

handlebars.registerHelper("delimiter", function (fields, index) {
  if(fields && fields.length-1 == index) {
    return ";";
  }
  return ",";
});

template 内容

enum.tpl 需要生成请求和返回以及引用到的文件定义。 you_package 需修改为你的目标包名

package your_package

// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改

{{!-- 模板使用到的自定义钩子: isInteger, delimiter }} --}}
 {{#each items}}
 {{#if desc}}
 /** {{desc}} */
 {{/if}}
 {{#if (isInteger)}}
 enum class {{name}}(val value: Int, val desc: String) {
    {{#each fields}}
    /** {{desc}} */
    {{ name }}({{value}}, "{{desc}}"){{delimiter ../fields @index}}
    {{/each}}
    companion object {
        fun fromValue(value: Int): {{name}}? {
            return values().find { it.value == value }
        }
    }
 }
 {{else}}
 enum class {{name}}(val value: String, val desc: String) {
    {{#each fields}}
    /** {{desc}} */
    {{ name }}("{{value}}", "{{value}}"){{delimiter ../fields @index}}
    {{/each}}
    companion object {
        fun fromValue(value: String): {{name}}? {
            return values().find { it.value == value }
        }
    }
 }
 {{/if}}

{{/each}}

.config.json5 参考配置

 {
    templates: [{
      modes: ["api"],
      template: "./templates/kt_enum.tpl",
      type: "enum",
      output: "./output/Enums.kt",
    }]
 }

生成代码例子

niuhe 定义

class StateEnum(ConstGroup):
    '''状态'''
    NORMAL = Item(1, '正常')
    DISABLED = Item(2, '禁用')
class AppEnum(ConstGroup):
    '''应用'''
    WEIXIN = Item("weixin", "微信")
    QYWX = Item("qywx", "企业微信")

kotlin 生成结果

 /** 状态 */
 enum class StateEnum(val value: Int, val desc: String) {
    /** 正常 */
    NORMAL(1, "正常"),
    /** 禁用 */
    DISABLED(2, "禁用");
    companion object {
        fun fromValue(value: Int): StateEnum? {
            return values().find { it.value == value }
        }
    }
 }
 /** 应用 */
 enum class AppEnum(val value: String, val desc: String) {
    /** 微信 */
    WEIXIN("weixin", "微信"),
    /** 企业微信 */
    QYWX("qywx", "企业微信");
    companion object {
        fun fromValue(value: String): AppEnum? {
            return values().find { it.value == value }
        }
    }
 }

生成 typescript enum 例子

生成 ts 常量不需要注入 handlebars 钩子, 下面常量模板

// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改

 {{#each items}}
 /** {{desc}} */
 declare enum {{name}} {
    {{#each fields}}
    /** {{desc}} */
    {{ name }} = {{value}},
    {{/each}}
 }

{{/each}}

.config.json5 参考配置

 {
    templates: [{
      modes: ["api"],
      template: "./templates/ts_enums.tpl",
      type: "enum",
      output: "./output/enums.d.ts",
    }]
 }

生成 typesctipy api 请求 例子

./request 为自定义的 api 工具类, 可在 langs 中添加"ts" 后生成一个参考例子。

template 编写


// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改

import { ajax_get, ajax_post, ajax_any } from "./request";

 {{#each items}}
 {{# if req}}
 /**
  * {{desc}} 
  * @path {{method}} {{url}}
  * @return {{capitalize ../mod}}.{{rsp.name}}
  {{#if codes}}
  * @codes 错误码如下
  {{#each codes}}
  * - {{name}}({{value}}) {{desc}}
  {{/each}}
  {{/if}}
*/
 export const {{name}} = (data: {{capitalize ../mod}}.{{req.name}}): Promise<Rsp<{{capitalize ../mod}}.{{rsp.name}}>> => {
    return ajax_any("{{method}}", "{{url}}", data);
 }
{{/if}}

{{/each}}

.config.json5 参考配置

 {
    templates: [{
      modes: ["api"],
      template: "./templates/ts_route.tpl",
      type: "route",
      output: "./output/route.ts",
    }]
 }

生成 typescript interface 例子

.templates.tpl.js 中添加 handlebars 自定义钩子

// templates 注入 例子。 不能使用 js 高级语法。
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/

handlebars.registerHelper('convertType', function (type) {
    const typeMap = {
        'string': 'string',
        'stringenum': 'string',
        'number': 'number',
        'float': 'number',
        'double': 'number',
        'int': 'number',
        'long': 'number',
        'enum': 'number',
        'integer': 'number',
        'boolean': 'boolean',
        'object': 'any',
        'array': 'any[]',
        'map': 'any',
    };
    return typeMap[type] || type;
});

template 编写


// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改

interface Rsp<T> {
    result: number
    data: T
    message: string
}

declare namespace {{capitalize mod}} {
 {{#each items}}
 {{#if desc}}
    /** {{desc}} */
 {{/if}}
    interface {{name}} {
        {{#each fields}}
        {{#if desc}}
        /** {{desc}} */
        {{/if}}
        {{#if (isArray label)}}
        {{ json }}: {{ convertType type }}[],
        {{else}}
        {{ json }}: {{ convertType type }},
        {{/if}}
        {{/each}}
    }

{{/each}}
}

.config.json5 参考配置

 {
    templates: [{
      modes: ["api"],
      template: "./templates/ts_messages.tpl",
      type: "message",
      output: "./output/messages.d.ts",
    }]
 }