160 lines
6.7 KiB
Markdown
160 lines
6.7 KiB
Markdown
# 用户 CRUD 参考项目
|
||
|
||
这个示例展示如何用 GoOmni 写一个完整业务模块:参数绑定、业务规则、存储接口、分页、认证授权、统一响应、错误转换和自定义中间件。
|
||
|
||
## 运行
|
||
|
||
在 `go-omni` 根目录执行(Go 1.22 或以上):
|
||
|
||
```sh
|
||
go run ./examples/reference
|
||
```
|
||
|
||
默认监听 `127.0.0.1:8080`,可通过环境变量调整:
|
||
|
||
```sh
|
||
GO_OMNI_ADDR=127.0.0.1:8081 go run ./examples/reference
|
||
```
|
||
|
||
如果终端提示找不到 `go`,需要先把 Go 的 `bin` 目录加入 `PATH`。不要与 `examples/basic` 同时占用同一个端口。
|
||
|
||
默认使用内存存储,启动时为空,重启后数据清空。写接口需要 `Authorization: Bearer dev-token`,删除接口还需要 admin 角色;这是本地演示 Token,不可用于生产。设置 `GO_OMNI_DB_DSN` 后会改用 MySQL 存储,启动时会连接并 Ping 数据库;请先执行 [`schema.sql`](schema.sql)。
|
||
|
||
## 分层
|
||
|
||
```text
|
||
examples/reference/
|
||
├── main.go # 组装存储、认证、业务服务、中间件和路由
|
||
├── handler/
|
||
│ ├── user.go # JSON / URI / Query 参数校验、HTTP 错误转换
|
||
│ └── middleware.go # 自定义结构化请求日志
|
||
├── service/
|
||
│ └── user.go # 业务规则、输入规范化、UserRepository 接口
|
||
├── repository/
|
||
│ ├── memory.go # 并发安全的内存实现,原子保证邮箱唯一
|
||
│ └── mysql.go # database/sql + MySQL 持久化实现
|
||
├── model/
|
||
│ ├── user.go # 用户、输入、分页结果
|
||
│ └── errors.go # 业务错误
|
||
└── main_test.go # 接口全流程与并发测试
|
||
```
|
||
|
||
调用路径:HTTP → Handler → Service → Repository。
|
||
|
||
- Handler 依赖 GoOmni,示例代码不导入 Gin,也不调用 `Engine()` / `Raw()`。
|
||
- Service、Repository、Model 不依赖 GoOmni 或 Gin,通过标准 `context.Context` 传递请求取消信号。
|
||
- JSON 校验标签留在 Handler 的请求结构上;业务模型没有 HTTP 绑定标签。
|
||
- Service 返回业务错误,由 Handler 映射为 HTTP 错误;未知错误交给框架转换成通用 500 响应。
|
||
- 通过 `application.Group("/api/v1", handler.RequestLog(logger))` 挂载自定义中间件,再注册 `/users` 子路由。日志记录请求 ID、耗时和返回错误;panic 由框架恢复中间件处理。
|
||
|
||
## 接口约定
|
||
|
||
| 方法 | 路径 | 成功结果 |
|
||
| --- | --- | --- |
|
||
| GET | `/health` | 200,健康检查 |
|
||
| POST | `/api/v1/users` | 201,创建用户 |
|
||
| GET | `/api/v1/users` | 200,分页列表 |
|
||
| GET | `/api/v1/users/:id` | 200,用户详情 |
|
||
| PUT | `/api/v1/users/:id` | 200,完整替换姓名和邮箱 |
|
||
| DELETE | `/api/v1/users/:id` | 204,无响应体 |
|
||
|
||
姓名和邮箱必填。姓名最长 100 个字符,去掉首尾空白后不能为空;邮箱最长 254 字节。本示例约定整个邮箱地址不区分大小写,按小写存储并唯一校验。
|
||
|
||
`PUT` 必须同时提交姓名和邮箱;ID 和时间戳由服务生成。删除后允许重新使用邮箱,新用户获得新的 ID。删除不存在的用户返回 404。
|
||
|
||
列表默认 `page=1&pageSize=20`,每页最多 100 条,按 ID 升序;超出最后一页返回空数组并保留总数。
|
||
|
||
## 试一遍完整流程
|
||
|
||
以下命令假定刚启动一个空实例,第一条创建的用户 ID 为 1:
|
||
|
||
```sh
|
||
# 健康检查
|
||
curl -i http://127.0.0.1:8080/health
|
||
|
||
# 创建:201(开发 Token)
|
||
curl -i -X POST http://127.0.0.1:8080/api/v1/users \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Authorization: Bearer dev-token' \
|
||
-H 'X-Request-ID: demo-create' \
|
||
-d '{"name":"张三","email":"zhangsan@example.com"}'
|
||
|
||
# 查询详情和分页列表:200
|
||
curl -i http://127.0.0.1:8080/api/v1/users/1
|
||
curl -i 'http://127.0.0.1:8080/api/v1/users?page=1&pageSize=10'
|
||
|
||
# 相同邮箱(包括大小写变体):409 EMAIL_TAKEN
|
||
curl -i -X POST http://127.0.0.1:8080/api/v1/users \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Authorization: Bearer dev-token' \
|
||
-d '{"name":"另一个人","email":"ZHANGSAN@example.com"}'
|
||
|
||
# 完整修改:200
|
||
curl -i -X PUT http://127.0.0.1:8080/api/v1/users/1 \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Authorization: Bearer dev-token' \
|
||
-d '{"name":"张三(已更新)","email":"zhangsan.new@example.com"}'
|
||
|
||
# 删除:204;随后查询:404 USER_NOT_FOUND
|
||
curl -i -X DELETE http://127.0.0.1:8080/api/v1/users/1 \
|
||
-H 'Authorization: Bearer dev-token'
|
||
curl -i http://127.0.0.1:8080/api/v1/users/1
|
||
```
|
||
|
||
分页结果的 `data` 结构:
|
||
|
||
```json
|
||
{
|
||
"items": [],
|
||
"total": 0,
|
||
"page": 1,
|
||
"pageSize": 20
|
||
}
|
||
```
|
||
|
||
成功与失败都使用框架的 `code`、`message`、`requestId` 约定;删除成功只有 HTTP 204 和请求 ID 响应头。
|
||
|
||
| HTTP 状态 | code | 说明 |
|
||
| --- | --- | --- |
|
||
| 400 | `INVALID_BODY` | JSON 或必填字段、格式校验失败 |
|
||
| 400 | `INVALID_PATH` | ID 不是正整数 |
|
||
| 400 | `INVALID_QUERY` | 分页参数不合法 |
|
||
| 400 | `INVALID_USER` | 业务校验失败,例如姓名全为空白 |
|
||
| 404 | `USER_NOT_FOUND` | 用户不存在 |
|
||
| 409 | `EMAIL_TAKEN` | 邮箱已被其他用户使用 |
|
||
| 500 | `INTERNAL_ERROR` | 未预期的内部错误,响应不包含内部细节 |
|
||
|
||
## 使用数据库
|
||
|
||
参考项目已经提供 MySQL 的 `service.UserRepository` 实现;Handler 和 Service 不需要修改。自定义数据库实现也应遵循相同契约:
|
||
|
||
- 创建和修改时原子保证规范化后的邮箱唯一,例如使用数据库唯一索引;将唯一键冲突转换为 `model.ErrEmailTaken`。
|
||
- 未找到用户返回 `model.ErrUserNotFound`;修改失败不保留部分更新。
|
||
- 列表按 ID 排序、返回总数,空列表使用 `[]`。
|
||
- 使用传入的 `context.Context` 执行查询,保留创建时间,修改时更新时间。
|
||
|
||
MySQL 运行配置通过环境变量传入:
|
||
|
||
```sh
|
||
export GO_OMNI_DB_DRIVER=mysql
|
||
export GO_OMNI_DB_DSN='root:password@tcp(127.0.0.1:3306)/go_omni?parseTime=true&loc=UTC'
|
||
export GO_OMNI_DB_MAX_OPEN_CONNS=10
|
||
export GO_OMNI_DB_MAX_IDLE_CONNS=5
|
||
export GO_OMNI_DB_PING_TIMEOUT=5s
|
||
go run ./examples/reference
|
||
```
|
||
|
||
不设置 `GO_OMNI_DB_DSN` 就使用内存实现。可通过 `GO_OMNI_DEV_TOKEN` 替换本地演示 Token。数据库连接会在收到 `SIGINT` / `SIGTERM` 后随 HTTP 服务优雅退出而关闭;连接失败会阻止服务启动,不会静默回退到内存存储。
|
||
|
||
## 验证
|
||
|
||
在框架根目录执行:
|
||
|
||
```sh
|
||
go test -race ./...
|
||
go vet ./...
|
||
go build ./...
|
||
```
|
||
|
||
测试覆盖完整 CRUD、认证和角色权限、校验失败、分页边界、邮箱冲突及失败更新、删除后重用邮箱、请求 ID、日志关联、内部错误隐藏、并发唯一性、取消请求不修改数据、数据库配置解析和 HTTP 优雅退出。MySQL 测试不自动连接外部数据库。
|