# 用户 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 ./... ``` `TestUserCRUD` 将同一份业务注册代码分别挂载到 Gin 与 nethttp,验证替换适配器后 Handler、Service、Repository 均无需修改。 测试覆盖完整 CRUD、认证和角色权限、校验失败、分页边界、邮箱冲突及失败更新、删除后重用邮箱、请求 ID、日志关联、内部错误隐藏、并发唯一性、取消请求不修改数据、数据库配置解析和 HTTP 优雅退出。MySQL 测试不自动连接外部数据库。