Files
go-omni/examples/reference/README.md

158 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 用户 CRUD 参考项目
这个示例展示如何用 GoOmni 写一个完整业务模块:参数绑定、业务规则、存储接口、分页、认证授权、统一响应、错误转换和自定义中间件。
## 运行
在 `go-omni` 根目录执行(Go 1.22 或以上):
```sh
go run ./examples/reference
```
默认读取 `examples/reference/config.yaml`,监听 `127.0.0.1:8080`。文件已显式引用地址变量:
```sh
GO_OMNI_HTTP_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,示例代码不导入具体网络库,也不调用 `Engine()` / `Raw()`。
- Service、Repository、Model 不依赖 GoOmni 或网络库,通过标准 `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` 执行查询,保留创建时间,修改时更新时间。
此手写示例显式读取 `GO_OMNI_DB_DSN`;驱动固定 mysql,连接池使用 database.DefaultConfig()。项目可将数据库配置放入自己的 Config 并通过 LoadProject 统一加载(见 [配置指南](../../CONFIG.md)),框架不再自动读取数据库环境变量:
```sh
export GO_OMNI_DB_DSN='root:password@tcp(127.0.0.1:3306)/go_omni?parseTime=true&loc=UTC'
go run ./examples/reference
```
不设置 `GO_OMNI_DB_DSN` 就使用内存实现。可通过 `GO_OMNI_DEV_TOKEN` 替换本地演示 Token。数据库连接会在收到 `SIGINT` / `SIGTERM` 后随 HTTP 服务优雅退出而关闭;连接失败会阻止服务启动,不会静默回退到内存存储。
## 验证
在框架根目录执行:
```sh
go test -race ./...
go vet ./...
go build ./...
```
`TestUserCRUD` 使用标准库适配器运行完整业务注册代码;同一套 Handler、Service、Repository 也可通过 `omni.NewWithRouter` 接入自研适配器。
测试覆盖完整 CRUD、认证和角色权限、校验失败、分页边界、邮箱冲突及失败更新、删除后重用邮箱、请求 ID、日志关联、内部错误隐藏、并发唯一性、取消请求不修改数据、数据库配置解析和 HTTP 优雅退出。MySQL 测试不自动连接外部数据库。