milady

command module
v0.6.19 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 10 Imported by: 0

README

AI 驱动的轻量化企业级 REST API 脚手架

这是一个专注于 REST API 的完整 CRUD 解决方案。

Milady 是一款 AI 驱动的轻量化企业级 REST API 脚手架,通过自然语言描述即可由 milady 生成领域代码,把重复样板交给 AI,开发者只需专注业务逻辑。

Milady 目标是:

  • 整洁架构,适用于各种规模的项目
  • 提供积木套装,快速开始项目,专注于业务开发
  • 令项目更简单,令研发心情更好
  • 学习曲线较低,开发者无需精通 DDD 也能快速上手

如果你觉得以上描述符合你的需求,那就快速开始吧。

支持代码自动生成:内置 milady gen 命令

设计说明

传统 MVC 单体架构,随着业务扩展,团队越难以有效开发,新入职的同事也很难去了解这个臃肿的单体。

模块化单体架构具有单体架构的许多优点和缺点,也具有微服务架构的大量优点少数缺点。

将完整业务拆分成多个领域模块,例如 用户领域 / 银行领域 / 商品领域,每个领域都有各自一套完善的

  • API(接口)
  • Core(业务)
  • Store(缓存/持久化存储)

不同的开发人员或团队,可以独立地处理这些领域模块,降低开发新功能而导致的冲突混乱。相比微服务而言,这样拆分模块代码,更小更简洁更易于测试。

当程序超出领域模块规模后,团队能够在需要时轻松地将领域模块提取到微服务中。

快速开始

# 安装 CLI
go install github.com/kalandramo/milady/cmd/milady@latest

# 创建新项目
milady init myapp -g github.com/yourname/myapp

# 运行项目
cd myapp && go run main.go

生成 DDD 分层代码:

# 基于结构体定义生成 Core/Store/Cache/API 代码
milady gen -f tables/user/user.go

# 多个文件
milady gen -f tables/user/user.go,tables/task/task.go

更多命令详见 cmd/milady/README.md

业务实践

业务说明:

假设我们要做一个数据库版本管理的业务,CRUD 步骤如下:

# 这里用 claude 演示,你可以用自己喜欢的 AI 编辑器
# 可以用更具体的业务说明,安装有 milady skills 时,会默认使用 milady 生成代码
claude -p "使用 milady 创建一个新的领域 version,表结构包含 id,created_at,updated_at,version"

静待业务完成


不使用 milady 时应该怎么做?

以 milady 生成效果为准,可以直接查看生成的相关代码。

在 「internal」-「core」 创建 「version」 目录,创建「version.model.go」写入领域模型,该模型为数据库表结构映射。

目录结构

├── internal                # 私有业务
│   ├── core                # 业务领域
│   │   └── version
│   │       ├── model.go    # 通用模型
│   │       ├── version.model.go   # 领域模型
│   │       ├── version.param.go   # 入参/出参结构体定义
│   │       ├── core.go     # 业务对象与接口
│   │       └── store
│   │           └── versiondb
│   │               ├── db.go       # 数据库入口/迁移/业务实例工厂
│   │               └── version.go  # 业务相关 CRUD 方法实现
│   └── web
│       └── api
│           ├── provider.go # 依赖注入 provider
│           └── version.go  # 路由注册

创建「core.go」 写入如下内容

package version

import (
	"fmt"
	"strings"
)

// Storer 依赖反转的数据持久化接口
type Storer interface {
	First(*Version) error
	Add(*Version) error
}

// Core 业务对象
type Core struct {
	Storer    Storer
}

// NewCore 创建业务对象
func NewCore(store Storer) *Core {
	return &Core{
		Storer: store,
	}
}

// IsAutoMigrate 是否需要进行表迁移
// 判断硬编码在代码中的数据库表版本号,与数据库存储的版本号做对比
func (c *Core) IsAutoMigrate(currentVer, remark string) bool {
	var ver Version
	if err := c.Storer.First(&ver); err != nil {
		isMigrate := true
		c.IsMigrate = &isMigrate
		return isMigrate
	}
	isMigrate := compareVersionFunc(currentVer, ver.Version, func(a, b string) bool {
		return a > b
	})
	c.IsMigrate = &isMigrate
	return isMigrate
}

func compareVersionFunc(a, b string, f func(a, b string) bool) bool {
	s1 := versionToStr(a)
	s2 := versionToStr(b)
	if len(s1) != len(s2) {
		return true
	}
	return f(s1, s2)
}

func versionToStr(str string) string {
	var result strings.Builder
	arr := strings.Split(str, ".")
	for _, item := range arr {
		if idx := strings.Index(item, "-"); idx != -1 {
			item = item[0:idx]
		}
		result.WriteString(fmt.Sprintf("%03s", item))
	}
	return result.String()
}

创建 「store/versiondb」 目录,创建「db.go」 文件写入

type DB struct {
	db *gorm.DB
}

func NewDB(db *gorm.DB) DB {
	return DB{db: db}
}

// AutoMigrate 表迁移
func (d DB) AutoMigrate(ok bool) DB {
	if !ok {
		return d
	}
	if err := d.db.AutoMigrate(
		new(version.Version),
	); err != nil {
		panic(err)
	}
	return d
}

func (d DB) First(v *version.Version) error {
	return d.db.Order("id DESC").First(v).Error
}

func (d DB) Add(v *version.Version) error {
	return d.db.Create(v).Error
}

在 API 层做依赖注入,对 「web/api/provider.go」 写入函数,往 Usecase 中注入业务对象

var ProviderSet = wire.NewSet(
	wire.Struct(new(Usecase), "*"),
	NewHTTPHandler,
	NewVersion,
)

func NewVersion(db *gorm.DB) *version.Core {
	vdb := versiondb.NewDB(db)
	core := version.NewCore(vdb)
	isOK := core.IsAutoMigrate(dbVersion, dbRemark)
	vdb.AutoMigrate(isOK)
	if isOK {
		slog.Info("更新数据库表结构")
		if err := core.RecordVersion(dbVersion, dbRemark); err != nil {
			slog.Error("RecordVersion", "err", err)
		}
	}
	// 其它组件可以调用此变量,判断是否需要表迁移
	orm.SetEnabledAutoMigrate(isOK)
	return core
}

在 API 层新建「version.go」文件,写入

// version 业务函数命名空间
type VersionAPI struct {
	ver *version.Core
}

func NewVersionAPI(ver *version.Core) VersionAPI {
	return VersionAPI{ver: ver}
}
// registerVersion 向路由注册业务接口
func registerVersion(r gin.IRouter, verAPI VersionAPI, handler ...gin.HandlerFunc) {
	ver := r.Group("/version", handler...)
	ver.GET("", web.WrapH(verAPI.getVersion))
}

func (v VersionAPI) getVersion(_ *gin.Context, _ *struct{}) (any, error) {
	return gin.H{"msg": "test"}, nil
}

团队规范

规范 说明 文档
API 设计 资源命名、HTTP 方法、状态码、分页、版本控制 api-design-patterns.md
Git 工作流 提交规范、分支策略、版本号、Changelog milady-git-workflow

接口文档

项目使用 AI 驱动的 code-first 方式生成 OpenAPI 3.1 文档,无需手写注释。当 API 层代码发生变动时,自动从路由注册(web.WrapH)、handler 签名、结构体 tag 中提取接口定义,生成 YAML 文档到 docs/api/ 目录。

详见 milady-api-doc 技能

如何实现Apifox 自动同步

生成的文档可自动推送到 Apifox。需配置:

  1. 设置环境变量 APIFOX_TOKEN(Apifox 个人访问令牌)
  2. 在项目根目录的 CLAUDE.mdAGENTS.md 中设置 APIFOX_PROJECT_ID=<你的项目ID>

配置完成后,文档变更时会自动执行同步脚本。未配置时跳过同步,不影响文档生成。

命名规范

类别 规范 示例
领域目录 全小写无分隔 versiontenant
领域文件 <领域名>.<用途>.go version.model.goversion.param.go
Store 目录 store/<领域名>db store/versiondb
Store 文件 db.go(入口/迁移) + <领域名>.go(CRUD) db.goversion.go
领域模型 PascalCase VersionTenant
入参结构体 <操作><领域>Input CreateVersionInput
出参结构体 <操作><领域>Output FindUserOutput
Core 对象 固定 Core type Core struct
Core 构造 NewCore(store Storer, opts ...Option)
Store 接口 固定 Storer type Storer interface
API 结构体 <领域>API VersionAPI
路由注册 Register<领域>(g gin.IRouter, api <领域>API, handler ...gin.HandlerFunc) RegisterVersion(...)
JSON tag snake_case json:"created_at"
路由路径 kebab-case + 复数名词 /api/v1/users
数据库表名 snake_case + 复数 versions

安全规范

SQL 注入防范

所有数据库操作使用 GORM 参数化查询,禁止拼接 SQL 字符串:

// 正确:参数化查询
db.Where("name = ?", input.Name).First(&user)

// 错误:字符串拼接
db.Where("name = '" + input.Name + "'").First(&user)

日志脱敏

日志输出中涉及用户敏感信息时必须脱敏处理:

字段类型 脱敏规则 示例
手机号 中间 4 位替换为 **** 138****1234
邮箱 @ 前仅保留首尾 a***z@example.com
密码/Token 禁止写入日志
身份证 仅保留首 3 末 4 110***1234

敏感数据加密

  • 密码必须使用 bcrypt 存储,禁止明文或 MD5
  • Token/密钥类配置通过环境变量注入,禁止硬编码在代码中
  • .envcredentials.json 等文件必须加入 .gitignore

单元测试

核心业务逻辑(Core 层导出函数)必须编写单元测试,确保关键路径有覆盖:

  • Store 接口天然支持 mock,测试时替换为 mock 实现
  • 测试文件与被测文件同目录,命名 <file>_test.go
  • 每个测试独立运行,不依赖其他测试的状态或执行顺序
  • 修复 bug 时先写复现测试,再修复

跨领域聚合

当业务涉及多个领域的数据时,根据性能要求和耦合度要求选择合适的方案:

模式 耦合度 适用场景
SQL 模式 查询聚合,Store 层直接连表查询其他领域的数据
命令编程模式 写操作聚合,领域 A 直接依赖领域 B,通过 tx 保证事务一致
API 层聚合模式 API 层协调多个 Core,各 Core 独立执行,结果在 API 层组装
适配器模式 领域间通过 Option 注入接口解耦,各自管理事务

SQL 模式

在 Store 层直接编写连表 SQL,适用于只读查询聚合:

func (d OrderDB) FindOrdersWithUser(ctx context.Context, userID string) ([]OrderWithUser, error) {
    var result []OrderWithUser
    return result, d.db.WithContext(ctx).
        Table("orders").
        Select("orders.*, users.name as user_name").
        Joins("LEFT JOIN users ON users.id = orders.user_id").
        Where("orders.user_id = ?", userID).
        Find(&result).Error
}

命令编程模式

Store 接口扩展事务方法,tx *gorm.DB 作为参数传入:

func (c *Core) CreateOrderAndDeduct(ctx context.Context, in CreateOrderInput) error {
    return c.db.Transaction(func(tx *gorm.DB) error {
        if err := c.store.CreateOrder(ctx, tx, in.Order); err != nil {
            return err
        }
        return c.store.DeductStock(ctx, tx, in.ProductID, in.Quantity)
    })
}

API 层聚合模式

API 层调用多个 Core 获取数据,在 API 层组装返回。列表场景下,将关联数据以 map 形式附在响应中,避免 N+1 查询:

func (a OrderAPI) listOrders(c *gin.Context, in *ListOrderInput) (*ListOrderOutput, error) {
    ctx := c.Request.Context()
    orders, err := a.orderCore.ListOrders(ctx, in.Pager)
    if err != nil {
        return nil, err
    }
    userIDs := uniqueUserIDs(orders) // 提取去重的用户 ID 列表
    users, _ := a.userCore.GetUserMap(ctx, userIDs)
    return &ListOrderOutput{
        Items: orders,
        Users: users, // map[string]UserBrief{"user_xxx": {Name: "张三"}}
		total: n,
    }, nil
}

适配器模式

通过 Option 注入适配器接口,领域间完全解耦。

详见 adapter-pattern.md

引用文章

Google API 设计指南

目录说明

.
├── main.go                 # 主函数入口
├── cmd                     # 可执行程序入口(一个入口时,不需要此目录)
├── configs                 # 配置文件
├── docs                    # 开发需求相关文档
│   └── api					# 接口文档
├── domain                  # 通用领域与基础业务
│   ├── token               # JWT 令牌、权限相关
│   ├── uniqueid            # 全局唯一 ID 生成器
│   └── version             # 数据库版本控制,避免每次启动都执行 gorm 迁移
│       ├── store/versiondb
│       └── versionapi
├── internal                # 私有业务
│   ├── app                 # wire 依赖注入组装
│   ├── conf                # 配置模型与默认值
│   ├── core                # 业务领域(示例/预留)
│   ├── data                # 数据库初始化
│   └── web
│       └── api             # RESTful API 路由注册
└── pkg                     # 项目依赖库
    ├── cmd                 # 命令行辅助
    ├── conc                # 并发工具
    ├── hook                # 常用函数钩子
    ├── logger              # 日志封装
    ├── orm                 # 数据库 ORM 封装
    ├── queue               # 队列实现
    ├── reason              # 错误原因定义
    ├── server              # HTTP 服务封装
    ├── system              # 系统工具
    └── web                 # Web 中间件、响应、校验等

项目说明

  1. 程序启动强依赖的组件,发生异常时主动 panic,尽快崩溃尽快解决错误。

  2. core 为业务领域,包含领域模型,领域业务功能

  3. store 为数据库操作模块,需要依赖模型,此处依赖反转 core,避免每一层都定义模型。

  4. api 层的入参/出参,可以正向依赖 core 层定义模型,参数模型以 Input/Output 来简单区分入参出数。

请求入参封装

完整函数参考:web-toolkit.md

本项目使用 GIN 作为 web 处理框架,路由函数需要实现 gin.HandlerFunc,在实现 API 层函数时,遇到的第一个问题是绑定参数,几乎每个函数都会涉及到反序列化,函数开头都充斥了 ctx.ShouldBindJSON 之类的代码。

根据 DRY(Don't Repeat Yourself)设计原则,通过减少重复代码来提高代码的可维护性和可重用性。该项目封装了 web.WrapH 其返回 gin.HandlerFuncweb.WrapH 的参数类似 GRPC,func(ctx *gin.Context, in *struct{}) (*Output, error)

WrapH 内部识别 POST/PUT/DELETE/PATCH 请求则绑定 Request Body,Get 请求则绑定 Request URL params。

入参第二个参数类型必须是指针,使用 *struct{} 表示没有参数,不需要绑定。在定义结构体时,尤其要注意结构体的 tag 应该是 json 或者 form,更多细节参考 GIN 框架参数绑定。

  • json 可绑定 request body 参数
  • form 可绑定 params 参数

返回值第一个参数是具体的 response body 内容,建议避免使用 any,其类型即可以是值,也可以是指针,赋予了更多灵活性。

当参数在多个位置时,即路由参数/查询参数/请求体参数同时存在,可以实现新的 web.WrapH2 或直接实现 gin.HandlerFunc

以下是两种代码的示例:

func findUser(ctx *gin.Context) {
	var in findUserInput
	if err := ctx.ShouldBindQuery(&in);err!=nil {
		ctx.JSON(...)
		return
	}
	out,err := serviceFunc(in)
	// ....
}
func findUsers(ctx *gin.Context, in *Input) (*Output, error) {
	return serviceFunc(in)
}

响应出参封装

明确的定义出参类型,可以使代码更容易读懂,我希望通过更多细节提升代码的可读性,可维护性。

web.WrapH 的封装默认是响应 application/json 类型。列表类接口返回空数据时应返回 [] 而非 null,避免前端解析异常。

在开发过程中,新同事实现 gin.HandlerFunc 时更容易遗忘 return 语句。使用 web.WrapH 能确保不遗落 return

以下是两种代码的示例

func findUsers(ctx *gin.Context) {
	// 可能 out 是从业务层获取的
	// 此时想知道 response body 需要往函数内部找
	out,err := serviceFunc()
	if err != nil {
		ctx.JSON(...)
		return
	}
	ctx.JSON(out)
}
func findUsers(ctx *gin.Context, in *Input) (*Output, error) {
	return serviceFunc(in)
}

错误处理

完整错误类型列表:web-toolkit.md

通过上面的代码了解到,错误是直接 return 的,难倒不担心底层的错误信息暴露给用户吗? 还有错误的 http statusCode 又是多少呢?

其实在 web.Warn 中还做了一些事情,比如在绑定过程中出错,可以定位到具体的错误原因,是类型不对? 错在哪个属性上? 比如响应的时候,通过 err 提取出信息,返回对应的 HTTP 状态码,接下来详细介绍错误处理。

pkg/web 是 HTTP 相关的处理包,包含中间件,响应,错误处理,鉴权,日志,限流,指标,性能分析,入参校验等等。

我们自定义一个 Error 类型, reason 是错误原因,有些第三方 API 也会用 Code。

该项目在设计的时候,考虑到状态码不易读,比如错误 10020,请问是什么错误? 所以定义了 reason,应该用大驼峰英语简略描述错误原因。那如果就是想用状态码表示呢? 请用 HTTP StatusCode。

msg 应当是开发者母语的错误描述,reason 用于程序内部判定,msg 用于友好提示给用户。details 是错误的扩展,提供给开发者,可以描述错误的解决方案,提供文档,错误的更细节详情,甚至暴露更底层的错误信息。

通常在前后端分离项目中,前端遇到一些错误,都需要询问后端发生了什么情况,通过 details 前端可以减少更多提问。

web.WrapH 的封装中,错误实际是调用的 web.Fail(err),此方法会判断 reason 应该返回怎样的 http statusCode,开发者可以在 pkg/web/error.goHTTPCode() 函数实现更多 http statusCode 扩展,已支持 200/400/401/403/429/500 六种状态码。

details 应该仅开发模式可见,web.SetRelease() 可以设置为生产发布模式,此时 details 将不会写入 http response body。

core 层导出的函数或 API 层返回的错误,应该返回 reason.Error 类型的错误。

在封装的 web.WrapH 中,会正确记录错误到日志并返回给前端。

func findUser(in *Input)  (*Output,error){
	// 数据库操作发生错误
	if err != nil {
		return nil, reason.ErrDB.SetMsg() // 错误的 respon 类型是 db 层错误,Msg 函数可以更改给用户的友好提示
	}
	// 业务发生错误
	if err != nil {
		return nil, reason.ErrServer.Withf("err[%s] ....",err) // Withf 可以写入 details 给开发者更多提示
	}
}

Makefile

如何安装 make? claude -p "安装 make 工具" 或自行搜索安装。Windows 为确保与 Linux 命令一致,请使用 Git Bash 终端,不要使用系统默认的 cmd/powershell 终端。

执行 makemake help 来获取更多帮助

在编写 makefile 时,应主动在命令上面增加注释,以 ## <命令>: <描述> 格式书写,具体参数 Makefile 文件已有命令。其目的是 make help 时提供更多信息。

makefile 中提供了一些默认的操作便于快速编写

make confirm 用于确认下一步

make title content=标题 用于重点突出输出标题

make info 获取构建版本相关信息

make wire 安装 wire 工具并重新生成依赖注入代码

make audit 格式化、检查、测试一条龙

make build/local 构建本地当前平台可执行文件,输出到 dist/<goos>_<goarch>

make docker/build 构建 docker 镜像

make docker/publish 构建 linux/amd64 与 linux/arm64 双平台镜像并推送仓库。使用前先修改 Makefile 中 IMAGE_NAME 为你的镜像仓库地址,例如 IMAGE_NAME := hub.docker.com/name/project:latest,存在多版本镜像时,可以修改 makefile 将 IMAGE_NAME 作为参数传入。

makefile 构建的版本号规则说明

  1. 版本号使用 Git tag,格式为 v1.0.0。

  2. 如果当前提交没有 tag,找到最近的 tag,计算从该 tag 到当前提交的提交次数。例如,最近的 tag 为 v1.0.1,当前提交距离它有 10 次提交,则版本号为 v1.0.11(v1.0.1 + 10 次提交)。

  3. 如果没有任何 tag,则默认版本号为 v0.0.0,后续提交次数作为版本号的次版本号。

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
cmd
milady command
cmd/milady/main.go 是 milady 命令行工具的入口文件, 目前只提供 init 子命令,用于从远程 milady 模板生成新项目。
cmd/milady/main.go 是 milady 命令行工具的入口文件, 目前只提供 init 子命令,用于从远程 milady 模板生成新项目。
milady/internal/cmd
Package cmd 注册并管理 milady 命令行工具的根命令及子命令。
Package cmd 注册并管理 milady 命令行工具的根命令及子命令。
domain
token
Code generated by milady, DO AVOID EDIT.
Code generated by milady, DO AVOID EDIT.
token/store/tokencache
Code generated by milady, DO AVOID EDIT.
Code generated by milady, DO AVOID EDIT.
token/store/tokendb
Code generated by milady, DO AVOID EDIT.
Code generated by milady, DO AVOID EDIT.
token/tokenapi
Code generated by milady, DO AVOID EDIT.
Code generated by milady, DO AVOID EDIT.
uniqueid
Code generated by milady, DO AVOID EDIT.
Code generated by milady, DO AVOID EDIT.
uniqueid/store/uniqueiddb
Code generated by milady, DO AVOID EDIT.
Code generated by milady, DO AVOID EDIT.
internal
app
pkg
assert
Package assert 提供最基础的测试断言函数,替代 testify/assert 的常用功能。
Package assert 提供最基础的测试断言函数,替代 testify/assert 的常用功能。
cmd
conc
concurrent 主要提供 3 个函数 GoRun,异步执行函数 Wait,等待所有任务执行完毕 UnsafeWaitWithContext,包含超时机制的等待所有任务执行完毕
concurrent 主要提供 3 个函数 GoRun,异步执行函数 Wait,等待所有任务执行完毕 UnsafeWaitWithContext,包含超时机制的等待所有任务执行完毕
orm
server
Package rlimit contains a function to raise rlimit.
Package rlimit contains a function to raise rlimit.
system
github.com/ixugo/netpulse 有更多关于 ip 的处理
github.com/ixugo/netpulse 有更多关于 ip 的处理
web
ws
ws/example command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL