<p align="center">
    <img src="./logo.png#gh-light-mode-only" alt="GoDDD Logo" width="550"/>
    <img src="./logo_dark.png#gh-dark-mode-only" alt="GoDDD Logo" width="550"/>
</p>

<p align="center">
    <a href="https://github.com/ixugo/goddd/releases"><img src="https://img.shields.io/github/v/release/ixugo/goddd?include_prereleases" alt="Version"/></a>
    <a href="https://github.com/ixugo/goddd/blob/master/LICENSE.txt"><img src="https://img.shields.io/dub/l/vibe-d.svg" alt="License"/></a>
	<a href="https://goreportcard.com/report/github.com/ixugo/goddd">
	<img src="https://goreportcard.com/badge/github.com/ixugo/goddd"/>
	</a>
	<a href="https://gin-gonic.com"><img width=30px  src="https://avatars.githubusercontent.com/u/7894478?s=48&v=4" alt="GIN"/></a>
    <a href="https://gorm.io"><img width=70px src="https://gorm.io/gorm.svg" alt="GORM"/></a>

</p>

[English](./README.md) | [简体中文](./README_zh.md)

# 企业 REST API 模板

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

GoDDD 目标是:

+ 整洁架构，适用于中小型项目
+ 提供积木套装，快速开始项目，专注于业务开发
+ 令项目更简单，令研发心情更好

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

支持[代码自动生成](github.com/ixugo/godddx)

支持[事件总线/事务消息](github.com/ixugo/nsqite)

## 设计说明

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

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

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

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

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

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


## 快速开始

1. Golang 版本 >= 1.23.0
2. `git clone github.com/ixugo/goddd`
3. `cd goddd && go build -o goddd ./cmd/server && ./goddd`
4. 新开一个终端访问 `curl http://localhost:8080/health`

5. 修改模块包名
`make rename name=github.com/name/project`
将 name 替换成实际的模块名



## 引用文章

[Google API Design Guide](https://google-cloud.gitbook.io/api-design-guide)

此项目的最佳实践: https://github.com/gowvp/gb28181

## 目录说明


```bash
.
├── main.go					主函数入口
├── cmd						更多可执行程序
├── configs					配置文件
├── docs					设计文档/用户文档
├── domain 					提供一些通用模型
│   ├── token 				token 过期与延迟，权限管理
│   ├── version 			数据库版本控制，避免每次启动执行 gorm 迁移
│   └── uniqueid			全局唯一 id 生成器
├── internal					私有业务
│   ├── conf					配置模型
│   ├── core					业务领域
│   │   └── version				实际业务
│   │       └── store
│   │           └── versiondb 		数据库操作
│   ├── data					数据库初始化
│   └── web
│       └── api					RESTful API
└── pkg						依赖库
```


## 项目说明

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

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

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

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


## 请求入参封装

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

根据 DRY（Don't Repeat Yourself）设计原则，通过减少重复代码来提高代码的可维护性和可重用性。该项目封装了 `web.WrapH` 其返回 `gin.HandlerFunc`，`web.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`。

以下是两种代码的示例:

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

```go
func findUsers(ctx *gin.Context, in *Input) (*Output, error) {
	return serviceFunc(in)
}
```

## 响应出参封装

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

`web.Warh` 的封装默认是响应 application/json 类型。

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

以下是两种代码的示例

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

```go
func findUsers(ctx *gin.Context, in *Input) (*Output, error) {
	return serviceFunc(in)
}
```

## 错误处理

通过上面的代码了解到，错误是直接 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.go` 中 `HTTPCode()` 函数实现更多 http statusCode 扩展，默认提供了 200/400/401 三种状态码。

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


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

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

```go
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

Windows 系统使用 makefile 时，请使用 git bash 终端，不要使用系统默认的 cmd/powershell 终端，否则可能会出现异常情况。

执行 `make` 或 `make help` 来获取更多帮助

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

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

`make confirm` 用于确认下一步

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

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

**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，后续提交次数作为版本号的次版本号。

## 库如何使用?

### hook.UseCache 临时缓存

old

```go
cache := make(map[string]string)
for i := range 10 {
	v, ok := cache[i]
	if ok {
		// 业务处理
		continue
	}
	v,err := fn()
	if err == nil {
		cache[v.ID] = v
	}
	// 业务处理
}
```

new

```go
	cacheFn :=  hook.UseCache(fn)
	for i := range 10 {
		v,_,err :=  cacheFn(i)
		if err == nil {
			// 业务处理
		}
	}
```

### hook.UseTiming 计算函数花销写日志

old

```go
	now := time.Now()
	// 业务处理夹杂在时间计算中
	if sub :=time.Since(now); sub > time.Second {
		slog.Error("函数名", "cost", cost)
	}else {
		slog.Debug("函数名", "cost", cost)
	}
```

new

```go
	cost := hook.UseTiming(time.Second)
	defer cost()

	// 业务处理
```

### hook.UseMemoryUsage 计算函数内存花销写日志

old

```go
	var m1, m2 runtime.MemStats
	runtime.GC()
	runtime.ReadMemStats(&m1)
	// 业务处理夹杂在时间计算中
	runtime.ReadMemStats(&m2)
	memUsed := m2.Alloc - m1.Alloc
	slog.Info("函数内存占用: ", "KB", float32(memUsed)/1024)
```

new
```go
	cost := hook.UseMemoryUsage(time.Second)
	defer cost()

	// 业务处理

```

### hook.FileMD5 计算文件的 md5

避免将文件全部读取到内存，按照 8k 缓存分块计算文件的 md5

### hook.UseTimer 灵活间隔的定时器

old

```go
func scheduleTask() {
	for {
		// 业务处理
		processTask()

		// 复杂的时间计算逻辑夹杂在业务中
		now := time.Now()
		nextRun := time.Date(now.Year(), now.Month(), now.Day()+1, 2, 0, 0, 0, now.Location()) // 明天凌晨2点执行
		if nextRun.Before(now) {
			nextRun = nextRun.Add(24 * time.Hour)
		}
		time.Sleep(nextRun.Sub(now))
	}
}
```

new

```go
func scheduleTask(ctx context.Context) {
	hook.UseTimer(ctx, processTask, func() time.Duration {
		return hook.NextTimeTomorrow(2, 0, 0) // 每天凌晨2点执行
	})
}

// 或者立即执行一次，然后按不同间隔执行
func scheduleTaskWithFirstRun(ctx context.Context) {
	nextTime := hook.NextTimeWithFirst(
		time.Second,        // 立即执行（1秒后）
		func() time.Duration {
			return 10 * time.Minute // 之后每10分钟执行一次
		},
	)
	hook.UseTimer(ctx, processTask, nextTime)
}
```

**更多 hook 直接看 pkg/hook 源码吧**




## 快速开始

业务说明:

假设我们要做一个版本管理的业务，curd 步骤如下:

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

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

```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」 文件写入

```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 中注入业务对象

```go
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」文件，写入

```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 层参数模型，表映射模型到底应该定义在哪里?

要清楚各层之间的依赖关系，api 直接依赖 core，db 依赖反转 core。故而领域模型定义在 core 中，api 的入参和出参也可以定义在 core，当然 core 层用不上的结构体，定义在 API 层也无妨。

> 为什么 api 层直接依赖 core 层，而不是依赖接口?

接口的目的是为了解耦，在实际开发过程中，更多是替换 api 层，而不是替换 core 层。

API 只做参数获取，返回响应参数，只做最少的事情，方便从 HTTP 快速过度的 GRPC。

面向未来设计，面向当下编程，先提高搬砖效率，等未来需要的那天会有更好的方式重构。

> 为什么 db 依赖反转 core?

数据持久化不是独立的，它为业务而服务。即持久化服务于业务，依赖于业务。

通过依赖反转，业务可以在中间穿插 redis cache 等其它 db 。

> 为什么入参/出参模型以  Input/Output 单词结尾

约定大于配置，类似有些项目以 Request/Response 单词结尾，只是为了有一个统一的，大家都明确的参数。

当然，有可能出参也是入参，你可以定义别名，也可以直接使用。

很多时候，我们都想明确自己在做什么，为什么这样做，这个「常见问题」希望能提供一点解惑思路。

> 如何为 goddd 编写业务插件?

```go
// RegisterVersion 有一些通用的业务，它们被其它业务依赖，属于业务的基层模块，例如表版本控制，字典，验证码，定时任务，用户管理等等。
// 约定以 Register<Core> 方式编写函数，注入 gin 路由，命名空间，中间件三个参数。
// 具体可以参考项目代码
func RegisterVersion(r gin.IRouter, verAPI VersionAPI, handler ...gin.HandlerFunc) {
	ver := r.Group("/version", handler...)
	ver.GET("", web.WrapH(verAPI.getVersion))
}
```

## 表迁移

每次程序启动都执行一遍，太慢了。

所以通过 version 表来控制，是否要进行表迁移操作。

当发现数据库表版本已经是最新时，即不执行。通过修改 api/db.go 文件中 dbVersion 控制版本号。

## 自定义配置目录

默认配置目录为可执行文件同目录下的 configs，也可以指定其它配置目录

`./bin -conf ./configs`


## 项目主要依赖

+ gin
+ gorm
+ slog / zap
+ wire

## 你也许还关心

[github.com/ixugo/amap 高德地图 API](https://github.com/ixugo/amap)
[github.com/ixugo/netpulse 免费API，查询IP信息](https://github.com/ixugo/netpulse)
[github.com/ixugo/bytepool 带引用计数的内存池](https://github.com/ixugo/bytepool)
