diff --git a/API.md b/API.md index 5bc77a9..eff342b 100644 --- a/API.md +++ b/API.md @@ -1,5 +1,7 @@ # API 契约与项目生成 +首次接入请先看 [README](README.md):包含固定远程版本安装、可运行项目、完整 `.api` 示例和已有服务接入步骤。本页用于查阅语法与生成规则;下方带 `--framework-dir` 的命令用于框架源码开发。 + 本阶段目标:从 `.api` 定义生成可运行的 Go 项目;定义、绑定、校验与 OpenAPI 保持一致;重复生成不覆盖业务实现;业务代码继续独立于网络层。实现入口是 `cmd/go-omni/apigen`,运行时契约编译器是 `contract`。 ## 与 go-zero 的对应关系 diff --git a/README.md b/README.md index a113669..8f2ce85 100644 --- a/README.md +++ b/README.md @@ -1,316 +1,378 @@ # GoOmni -GoOmni 是一套面向业务复用的 Go 后端基础框架。当前默认使用只依赖标准库的网络适配器,业务代码只依赖 GoOmni 的 transport-neutral API,后续可以替换为自研网络层。 +GoOmni 是面向多个业务项目复用的 Go 后端框架:用 `.api` 定义接口、请求和返回类型,生成项目目录、HTTP 接入代码及 OpenAPI,业务代码负责 Service 和 Repository。也可以在已有项目中直接使用 `omni.New()`,或单独引入日志、存储等模块。 -远程 module 地址是 `git.yuntuo.hk/k2safe/go-omni`。业务项目可以直接引用指定版本: +**从这份 README 开始即可接入。** 当前版本为 **`v0.8.0-preview`**,Go **1.22+**;CLI 和项目依赖使用同一固定版本。框架不依赖 Gin,默认使用标准库网络层,提供可切换的 native HTTP/1.1 和实验性 HTTP/2 适配器。 + +响应字段、业务码、登录方式、权限规则和数据库表结构由接入项目决定。文中的 `code/msg/data`、`200/9999` 都是示例配置,可以替换。 + +## 选择接入方式 + +| 你的项目 | 从哪里开始 | +| --- | --- | +| 新建服务,先定义接口再开发 | [新项目:生成并运行](#新项目生成并运行) | +| 已有服务,逐步接入 HTTP 或基础模块 | [已有项目:按模块接入](#已有项目按模块接入) | +| 想统一返回体、业务错误或三方回调 | [配置公共响应与独立协议](#配置公共响应与独立协议) | +| 要接日志、数据库、鉴权和存储 | [业务开发与依赖装配](#业务开发与依赖装配) | +| 要切换网络层、配置 TLS 和退出清理 | [配置与运行](#配置与运行) | +| 已接入,需要升级和检查生成代码 | [日常开发与升级](#日常开发与升级) | + +## 准备环境与安装 + +module 地址是 `git.yuntuo.hk/k2safe/go-omni`。先确保本机 Go 和 Git 可用,并具有仓库读取权限;使用自己的 Git 凭据管理器,不把账号或 Token 写进 module 地址。 + +以下命令在同一个终端执行。已有 `GOPRIVATE` 时,把 `git.yuntuo.hk` 追加到原来的逗号分隔列表。 + +```sh +export GOPRIVATE=git.yuntuo.hk +go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.8.0-preview +export PATH="$(go env GOPATH)/bin:$PATH" +go-omni --help +``` + +如果配置了自定义 `GOBIN`,将那个目录加入 PATH。只引用框架包、不生成项目时可以跳过 CLI 安装,直接使用下文的 `go get`。 + +## 新项目:生成并运行 + +在准备存放业务项目的目录执行,`./hello` 必须是新目录或空目录;把 `example.com/hello` 换成自己的 Go module 名称。 + +```sh +go-omni api new --framework-version v0.8.0-preview \ + --module example.com/hello ./hello +cd hello +go mod tidy +go run ./cmd/server +``` + +另开终端验证: + +```sh +curl -i http://127.0.0.1:8080/api/greet/Alice +curl -i http://127.0.0.1:8080/health/ready +curl -sS http://127.0.0.1:8080/openapi.json +``` + +第一个接口返回 HTTP 200,业务数据为 `{"message":"Hello Alice"}`。初始模板未配置响应格式,使用默认公共体: + +```json +{"code":"OK","message":"success","data":{"message":"Hello Alice"},"requestId":"本次请求编号"} +``` + +按 Ctrl+C 停止服务。项目依赖已固定为远程版本,不需要克隆框架源码或添加本地 `replace`。 + +### 生成的项目放什么 + +```text +hello/ +├── api/service.api # 维护接口定义,也可 import 其他 .api +├── cmd/server/main.go # 启动、路由注册、优雅退出 +├── etc/config.yaml # 项目配置 +├── internal/ +│ ├── settings/ # 配置读取 +│ ├── servicecontext/ # 数据库、日志、外部客户端等依赖 +│ ├── types/typesGen.go # 生成的请求与业务返回类型 +│ ├── handler/ # 生成的绑定、响应策略、路由及文档 +│ ├── service/greet/ # 在这里实现业务逻辑 +│ ├── repository/greet/ # 在这里实现数据访问 +│ └── middleware/ # 声明鉴权等中间件后生成对应 hook +├── docs/openapi.json # 生成的 OpenAPI 3.0.3 +└── .goOmni/apiManifest.json # 生成文件所有权与校验信息,一起提交 Git +``` + +修改 `api/service.api` 后重新生成,业务写在 `internal/service`,数据访问写在 `internal/repository`,依赖在 `internal/servicecontext` 装配。声明业务错误后还会生成 `internal/apierrors/errorsGen.go`。 + +`api new` 的问候接口可以直接运行;自定义接口新生成的 Service 和鉴权 hook 返回 **501**,需要实现后才可用。生成器不会根据 HTTP 字段猜数据库表或业务规则。 + +### 从自己的接口定义生成 + +保存下面的完整示例为 `catalog.api`,再执行后面的生成命令。这里的响应格式、错误码和提示都由这个业务项目定义。 + +```text +syntax = "v1" + +response CommonResponse ( + body: {"code":"${code}", "msg":"${message}", "data":"${data}"} + codeType: int32 + successCode: 200 + errorCode: 9999 + successMessage: "操作成功" + dataMode: always + codes: {"ITEM_NOT_FOUND":404001} +) + +error ItemNotFound 404 "ITEM_NOT_FOUND" "记录不存在" + +type GetItemReq { + ItemID string `path:"itemId" binding:"required"` +} +type ItemResp { + ItemID string `json:"itemId"` + Name string `json:"name"` +} + +@server ( + group: item + prefix: /api/v1 + response: CommonResponse +) +service catalog-api { + @doc "查询商品" + @errors ItemNotFound + @handler getItem + get /items/:itemId (GetItemReq) returns (ItemResp) +} +``` + +```sh +go-omni api validate --api catalog.api +go-omni new --api catalog.api --framework-version v0.8.0-preview \ + --module example.com/catalog ./catalog +cd catalog +go mod tidy +``` + +将生成的 `internal/service/item/getItem.go` 替换为下面的演示实现。实际业务中换成 Repository 查询,Service 返回业务数据或错误,不再手工包装公共体: + +```go +package item + +import ( + "context" + + "example.com/catalog/internal/apierrors" + "example.com/catalog/internal/types" +) + +func (s *Service) GetItem(ctx context.Context, req *types.GetItemReq) (*types.ItemResp, error) { + if req.ItemID != "1001" { + return nil, apierrors.ItemNotFound() + } + return &types.ItemResp{ItemID: req.ItemID, Name: "示例商品"}, nil +} +``` + +示例 import 对应上面的 `example.com/catalog`,使用其他 module 名称时同步替换。启动后,在另一个终端验证: + +```sh +# catalog 项目目录中启动;先停掉前面的 hello 服务,释放 8080 端口。 +go run ./cmd/server +# 另一个终端: +curl -i http://127.0.0.1:8080/api/v1/items/1001 +curl -i http://127.0.0.1:8080/api/v1/items/missing +``` + +成功时得到 `{"code":200,"msg":"操作成功","data":{"itemId":"1001","name":"示例商品"}}`;不存在时为 HTTP 404、`{"code":404001,"msg":"记录不存在","data":null}`。 + +需要 MySQL、文件存储或文件日志时,**首次生成**可以使用: + +```sh +go-omni new --api catalog.api --framework-version v0.8.0-preview \ + --module example.com/catalogFull --database mysql --storage local \ + --log-output file --log-format json ./catalogFull +``` + +该命令在 `catalog.api` 所在目录执行,目标仍需是新目录。数据库迁移、表结构和 Repository 由项目维护,API 模板不自动建表;MySQL DSN 通过 `GO_OMNI_DB_DSN` 提供。存储还可选 `s3`、`r2`、`oss`,按生成项目的配置说明提供桶、端点和凭据。完整装配例子见 [userService](examples/userService/README.md)。 + +不采用 `.api` 时,也可以用 `go-omni new --framework-version v0.8.0-preview --module example.com/billing ./billing` 创建手写接口的 CRUD 示例项目;两种生成方式选择一种即可。 + +## 已有项目:按模块接入 + +在业务项目的 **go.mod 所在目录**执行: ```sh go get git.yuntuo.hk/k2safe/go-omni@v0.8.0-preview ``` -`v0.4.0` 移除了 Gin 和 Gin 专属的 `WithMode` 配置;`omni.New()` 直接使用标准库路由,已有业务 Handler / Service 无需修改。 +已有项目可直接复用现有目录、Service、Repository 和数据库连接,先选择一个接口接入。也可以只用 `logx`、`storage` 等包,暂时保留原来的 HTTP 框架。 -创建新业务项目: - -```sh -go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.8.0-preview -go-omni new --module example.com/billing ./billing -cd billing -go mod tidy -go run ./cmd/server -``` - -远程安装的 CLI 默认引用自身版本,生成的项目不需要本地 `replace`。 -具体版本和本地开发选项见 [CLI 文档](cmd/go-omni/README.md)。 - -需要数据库持久化时,使用 `go-omni new --database mysql --module example.com/billing ./billing`。 -生成项目包含完整用户 CRUD、数据库 Repository、ServiceContext、建表脚本和就绪探针。 -先应用生成的 `schema.sql`,再配置 `GO_OMNI_DB_DSN`;启动时会检查连接与表结构,退出时释放连接池。 - -## API 定义与项目生成 - -当前源码新增 **API 定义驱动项目生成**:写 `.api` 定义请求、响应和路由,自动生成项目目录、DTO、Handler、Service 骨架及 OpenAPI。重复生成保留业务代码,支持只读 CI 检查,完整语法和示例见 [API 开发指南](API.md)。 - -```sh -go run ./cmd/go-omni api new --framework-dir . \ - --module example.com/omniDemo ../omniDemo -``` - -API 生成支持 `--database mysql --storage local --log-output file` 组合初始化。上面的命令使用本地源码;远程安装见 [发布说明](RELEASE.md),完整业务案例见 [userService](examples/userService/README.md)。 - -## 目录结构 - -```text -go-omni/ -├── app/ # 应用启动和生命周期 -├── httpx/ # 与网络框架无关的 HTTP 抽象 -├── response/ # 可配置公共体和响应编码器 -├── errors/ # 统一错误 -├── health/ # 存活和就绪检查 -├── middleware/ # 请求 ID 等基础中间件 -├── logx/ # 业务层结构化日志 -├── metrics/ # 请求指标与状态统计 -├── config/ # 配置管理 -├── validate/ # 参数校验 -├── contract/ # API 字段契约、严格多源绑定与文档约束 -├── auth/ # 认证和权限接口 -├── database/ # database/sql 生命周期和事务 -├── cache/ # 有界内存缓存、Redis 适配与合并回源 -├── httpc/ # 出站 HTTP 连接池、超时和重试 -├── task/ # 有界后台任务与间隔调度 -├── queue/ # 持久队列接口与 Redis Streams 适配 -├── storage/ # Local、S3/R2、OSS 与临时下载链接 -├── openapi/ # 路由和 OpenAPI 3.0.3 契约 -├── telemetry/ # 可注入 OpenTelemetry 观测 -├── resilience/ # 熔断与上游并发隔离 -├── audit/ # 显式业务审计与可注入 Sink -├── pagination/ # 有界分页和统一结果 -├── transport/nethttp/ # 标准库网络适配器 -├── transport/native/ # 自研 HTTP/1.1 服务端预览 -├── transport/http2/ # opt-in HTTP/2 实验适配器 -├── examples/ # 示例项目 -└── cmd/ # go-omni 项目脚手架 -``` - -main 分支新增缓存、出站 HTTP、限流/幂等/超时中间件、后台任务和 `go-omni add` 模块脚手架。 -使用方式、默认容量和进程内能力的适用范围见 [业务基础模块](MODULES.md), -可以直接运行 [组合示例](examples/modules/README.md)。这些能力从 `v0.7.0-preview` 提供。 - -扩展模块还包括 JWT/Session、Policy、文件配置及安全热更新、Redis 共享中间件、持久队列、多平台对象存储、OpenAPI、OpenTelemetry、熔断、迁移、审计和分页。 -使用与边界见 [扩展模块](ADVANCED.md),可运行 [文件服务组合示例](examples/platform/README.md)。go-omni 不集成 Orbit;需要流程编排的业务项目独立接入。 - -## 快速开始 +下面是可独立运行的 `main.go` 示例;迁入现有项目时,把路由调用接到自己的业务服务即可。 ```go package main -import omni "git.yuntuo.hk/k2safe/go-omni" - -func main() { - app := omni.New() - - app.GET("/health", func(ctx *omni.Context) error { - return ctx.OK(map[string]string{"status": "ok"}) - }) - - app.Run(":8080") -} -``` - -成功响应默认使用统一结构: - -```json -{ - "code": "OK", - "message": "success", - "data": {"status": "ok"}, - "requestId": "..." -} -``` - -业务错误可以直接返回: - -```go -return omni.NotFound("USER_NOT_FOUND", "user was not found") -``` - -默认网络适配器位于 `transport/nethttp`。业务层不需要导入具体网络库;如果后续实现自己的网络层,只需要实现 `httpx.Router` / `httpx.RouterGroup`,然后通过 `omni.NewWithRouter` 启动应用: - -```go -application := omni.NewWithRouter(myRouter) -``` - -仓库还提供了使用标准库路由的 `transport/nethttp` 参考实现。两种适配器已通过同一套路由、业务处理链和取消传播测试,完整用户 CRUD 复用同一份 Handler / Service。 - -自研适配器可以直接运行 `transport/transporttest.Run` 验收。具体规则、v0.4.0 的斜杠 / Any / 405 行为调整,以及自研网络内核与当前 HTTP 接口的边界,见 [适配层契约](transport/README.md)。 - -`app.Engine()` 返回标准 `http.Handler`,`ctx.Raw()` 返回适配器提供的 `http.ResponseWriter`,业务 API 不锁定具体网络实现。 - -需要试用第一版自研网络层时,业务注册代码无需改变,只替换启动器: - -```go import ( - "context" - "log" - "os" - "os/signal" - "syscall" + "log" + "net/http" - omni "git.yuntuo.hk/k2safe/go-omni" - "git.yuntuo.hk/k2safe/go-omni/transport/native" + omni "git.yuntuo.hk/k2safe/go-omni" + "git.yuntuo.hk/k2safe/go-omni/logx" + "git.yuntuo.hk/k2safe/go-omni/response" ) -ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) -defer cancel() +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} -app := omni.New() -app.GET("/health", func(ctx *omni.Context) error { - return ctx.OK(map[string]string{"status": "ok"}) -}) -if err := app.RunWith(ctx, native.New(nil), ":8080"); err != nil { - log.Fatal(err) +func run() error { + closeLog, err := logx.Setup(logx.Config{ + Service: "catalog", Level: "info", Format: "json", Output: "stdout", + }) + if err != nil { + return err + } + defer closeLog() + + app := omni.New( + omni.WithLogger(true), + omni.WithResponse(response.Config{ + Body: map[string]any{ + "code": "${code}", "msg": "${message}", "data": "${data}", + }, + CodeType: "int32", SuccessCode: 200, ErrorCode: 9999, + SuccessMessage: "操作成功", DataMode: "always", + Codes: map[string]any{"ITEM_NOT_FOUND": 404001}, + }), + ) + app.GET("/api/v1/items/:itemId", func(c *omni.Context) error { + itemID := c.Param("itemId") + logx.InfoContext(c.Context(), "item.get", "itemId", itemID) + if itemID != "1001" { + return omni.NotFound("ITEM_NOT_FOUND", "记录不存在") + } + return c.OK(map[string]string{"itemId": itemID, "name": "示例商品"}) + }) + // 第三方协议可明确输出文本,不经过公共 JSON 包装。 + app.GET("/ping", func(c *omni.Context) error { + return c.Text(http.StatusOK, "pong") + }) + return app.Run("127.0.0.1:8080") } ``` -`transport/native` 当前支持 HTTP/1.1、Keep-Alive、`Content-Length` 和 chunked 请求体、`http.Flusher` 流式响应、可选 TLS、读写超时和优雅关闭;不支持的传输编码会被拒绝。它会复用连接状态和响应对象,并可通过 `MaxConnections`、`MaxConcurrentRequests` 限制内存峰值。它仍通过标准 HTTP 请求/响应接口连接业务层,适合在现有 Handler / Service 上做网络层验证。 +运行 `go run .`,访问 `/api/v1/items/1001`、`/api/v1/items/missing` 和 `/ping`,可分别验证成功、业务错误及纯文本输出。`App` 实现标准 `http.Handler`,也可挂进现有 `http.Server` 或父路由;这时监听、TLS、信号和资源关闭由现有入口负责,结束时调用 `app.Close()`。 -native TLS 当前只支持并宣告 ALPN `http/1.1`;配置 `h2` 会在启动时快速失败。需要 HTTP/2 时,可由 Nginx/Cloudflare 终止后回源 HTTP/1.1,或使用默认的 `transport/nethttp` 适配器。 +接入现有业务时,逐项确认: -`transport/http2` 是独立的 opt-in 实验适配器,使用 `x/net/http2` 处理帧和流控,同时保留 GoOmni 的连接上限、全局请求并发闸门和生命周期边界。它不会改变 native 默认路径;实验入口、限制和验收结果见 [HTTP/2 进入条件](transport/native/HTTP2.md)。 +- **协议**:保留既有 URL、HTTP 状态、字段、分页和空值语义。已有业务错误类型需要在项目内转换为 Omni 错误,或使用 `WithResponseEncoder` 适配原错误;设置字段模板不会自动识别其他库的错误。 +- **鉴权**:把现有登录态和权限校验接入中间件,继续使用原来的授权范围和业务规则。原来依赖 `gin.Context` 等类型的 HTTP 代码需要适配;标准 `context.Context` 的业务服务可以继续复用。 +- **数据类型**:ID、金额、日期等按项目协议转换。`.api` 当前不支持外部 Go 类型、自定义 JSON 编解码及 `json:",string"`;需要 JSON 字符串时在契约中声明 `string`,在业务层显式转换。 +- **生成器**:它不会自动接管任意已有 Go 项目。希望引入 `.api` 时,先在独立新目录生成对照,再规划路由与生成目录的接入,不要用脚手架覆盖现有业务目录。 -自研网络层还提供 `server.Stats()`,可读取 accepted、rejected、active connections 和 active requests,与上面的业务请求 metrics 组合后能区分连接堆积、过载拒绝和 Handler 慢请求。`rejectedConnections` 表示连接闸门拒绝数,`rejectedRequests` 表示并发请求闸门返回 503 的次数。 +先通过一个接口的成功、参数错误、未登录、无权限及业务错误回归,再按模块迁移。框架不要求所有项目使用同一套业务目录或身份模型。 -业务代码可以直接使用 `git.yuntuo.hk/k2safe/go-omni/logx` 打结构化日志,不需要依赖具体网络库: +## 配置公共响应与独立协议 -```go -func (s *UserService) Create(ctx *httpx.Context) error { - logx.InfoContext(ctx.Context(), "user.create", "email", input.Email) - return nil -} -``` - -需要基础运行指标时,可以注册 transport-neutral metrics middleware 和接口: - -```go -requestMetrics := omni.NewMetrics() -app.Use(requestMetrics.Middleware()) -app.GET("/metrics", requestMetrics.Handler()) -``` - -接口返回请求总数、错误数、当前处理中请求、累计耗时和 HTTP 状态分布。它是进程内快照,生产环境可由业务侧定时采集到监控系统。 - -`WithLogger(true)` 会启用 GoOmni 的统一 HTTP 访问日志;日志内容和业务日志共用 `requestId`。应用启动时可以调用 `logx.Setup` 配置 JSON/文本格式、级别、服务名和输出文件。文件轮转、敏感字段脱敏、异常堆栈与慢请求配置见 [logx 文档](logx/README.md);文件直传与云端分片上传见 [storage 文档](storage/README.md)。 创建新业务项目时可通过 `go-omni new --log-output file --storage local` 按需生成配置、装配代码与可运行示例,当前源码使用方式见 [CLI 文档](cmd/go-omni/README.md)。 - -应用配置可以统一从 `GO_OMNI_*` 环境变量加载,再交给 `omni.New`: - -```go -settings, err := omni.LoadConfig("") -if err != nil { - log.Fatal(err) -} -logger, err := logx.New(settings.Log) -if err != nil { - log.Fatal(err) -} -application := omni.New(omni.WithConfig(settings)) -application.OnShutdown(logger.Close) -``` - -常用变量包括 `GO_OMNI_ENABLE_LOGGER`、`GO_OMNI_LOG_LEVEL`、 -`GO_OMNI_LOG_FORMAT`、`GO_OMNI_LOG_OUTPUT`、`GO_OMNI_SERVICE`、 -`GO_OMNI_ENVIRONMENT`、`GO_OMNI_HTTP_ADDR`、`GO_OMNI_SHUTDOWN_TIMEOUT` 和 -`GO_OMNI_TRUSTED_PROXIES`。可信代理变量使用逗号分隔的 IP/CIDR,例如 -`GO_OMNI_TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8`;只有来自这些地址的连接才会解析 -`X-Forwarded-For`,默认只记录 socket 对端地址。数据库连接仍由 -`database.ConfigFromEnv` 从 `GO_OMNI_DB_*` 加载。 - -网络层也由同一份配置选择,Handler / Service / Repository 不需要导入具体适配器: - -| 配置 | 默认值 / 用途 | +| 接口来源或需求 | 配置位置 | | --- | --- | -| `GO_OMNI_TRANSPORT` | `nethttp`;可选 `native`、`http2` | -| `GO_OMNI_TLS_CERT_FILE` | TLS 证书 PEM 文件路径 | -| `GO_OMNI_TLS_KEY_FILE` | TLS 私钥 PEM 文件路径,必须与证书一起配置 | -| `GO_OMNI_SHUTDOWN_TIMEOUT` | `10s`,三种适配器共用的请求排空时间 | +| `.api` 生成的接口 | `response Name (...)` 定义,`@server (response: Name)` 引用 | +| 多个 `.api` 共用返回体 | 将声明放到 `common.api`,使用 `import "common.api"` | +| 某个生成接口用另一种 JSON 公共体 | 路由声明 `@response OtherResponse` | +| 只要业务 JSON、无成功包装 | 路由声明 `@response raw`;失败仍沿用组级格式 | +| 手写路由和应用级 404/405 | `omni.WithResponse(response.Config{...})`,或读取配置后传入 `WithConfig` | +| 成功和失败都要遵循特殊回调协议 | `HandleWithResponse` 绑定项目自己的 `response.Encoder` | +| 单次直接输出 JSON、文本、XML、字节或空响应 | `c.JSON` / `c.Text` / `c.XML` / `c.Bytes` / `c.NoContent` | -例如运行自研 HTTP/1.1,或显式试用独立 HTTP/2 适配器: +`${data}` 是业务返回值,`${message}` 是公开提示,`${code}` 是业务码;占位符保留原值类型,不是字符串拼接。还支持 `${requestId}`、`${ok}`、`${status}`,字段名和嵌套结构由项目决定。 -```sh -GO_OMNI_TRANSPORT=native go run ./examples/reference -GO_OMNI_TRANSPORT=http2 GO_OMNI_HTTP_ADDR=127.0.0.1:8443 \ - GO_OMNI_TLS_CERT_FILE=./server.crt GO_OMNI_TLS_KEY_FILE=./server.key \ - go run ./examples/reference -``` +- `codeType` 可以是 `string`、`int32`、`int64`;整数类型需要配置 `successCode` 和 `errorCode`。 +- `codes` 把逻辑错误码映射成对外业务码;未单独映射的失败使用 `errorCode`。JSON 业务码不会改变 HTTP 状态,例如 `404001` 仍对应 HTTP 404。 +- `successMessage` 设置成功提示;未设置 `errorMessage` 时沿用错误的公开提示。`messages` 可按逻辑错误码单独覆盖,优先级最高。全局 `errorMessage` 会替换未单独映射的所有失败提示,只有需要统一失败文案时才设置。 +- `dataMode: always` 在无数据时输出 null;默认 `omitNil` 省略模板里相应对象字段。没有 `returns` 的 `.api` 路由默认 HTTP 204、无响应体,这与 `200 + data:null` 是不同的接口契约。 -`http2` 必须配置 TLS;native TLS 只协商 `http/1.1`,nethttp TLS 支持标准库的 HTTP/1.1 和 HTTP/2。 -未知适配器、缺少配对证书、无法加载证书时,`Run` / `RunContext` 在监听前返回错误并执行资源清理,不会退回其他协议。 -HTTP/2 仍为实验适配器,尚未合并到 native 多协议监听器。 +**生成接口的格式以 `.api` 为准**,修改后重新生成,运行时与 OpenAPI 一起更新;应用 YAML 不会覆盖它。404/405 没有匹配到某个 `.api` 接口,使用应用默认配置,需要统一时也配置应用默认值。 -代码配置可写成 `omni.New(omni.WithTransport(omni.TransportNative), omni.WithTLS(certFile, keyFile))`。 -`ServeContext(ctx, listener)` 使用相同配置并接管已有 listener;失败或退出时也会关闭它。 -`RunWith(ctx, runner)` 保留自定义网络层入口,由 runner 自己管理 TLS、超时和网络参数;连接/请求上限等适配器专属选项仍通过这个入口设置。 -`NewServer` 仅创建标准库明文服务器,配置选择应用于 `Run`、`RunContext` 和 `ServeContext`。 -三种适配器的配置、真实 TLS 业务和关闭行为已通过 Linux 全量 race 及 10 次重复回归,见 [统一启动验收](performance/transportStartup.md)。 +文本/XML 回调用 Go 编码器或直接输出,并维护对应文档;`.api` 的 `raw` 仅表示原始 JSON。前面示例的 `c.Text` 控制这次正常返回;需要连鉴权失败、业务错误和 panic 都按回调协议响应时使用 `HandleWithResponse`。完整配置和回调示例见 [响应参考](RESPONSE.md)。 -`App` 同时实现标准 `http.Handler`。`Run` 监听 `SIGINT` / `SIGTERM`;`RunContext` 则由调用方的 context 控制退出,生成项目已使用 `signal.NotifyContext` 接好信号。 -退出时先停止接收新请求,等待活跃请求完成,再执行资源清理。超过 `ShutdownTimeout` 时强制关闭连接、取消请求 context,并返回超时错误。业务代码应响应 context 取消;框架无法强制结束忽略取消的业务 goroutine。数据库连接池由 `database.Open` 创建,在服务退出时调用 `Close`。 +## 业务开发与依赖装配 -应用资源可以直接挂到 `omni.New` 返回的生命周期上: +### 日志 + +业务包直接使用 `logx`,不需要 HTTP 框架对象: ```go -application := omni.New() -application.OnShutdown(db.Close) +// ctx 是 Service 接收的标准 context.Context。 +logx.InfoContext(ctx, "order.created", "orderId", orderID) +logx.ErrorContext(ctx, "order.failed", "error", err) ``` -清理函数会按注册顺序的逆序执行,`application.Close()` 和 `RunContext` 都会触发,且重复调用安全。 +入口用 `logx.Setup` 初始化,退出时关闭;生成的 API 项目已经完成日志初始化和退出清理。Service、Repository 和外部请求持续传递请求的 `context.Context`,业务日志可关联 `requestId`,同时保留取消信号。框架不规定或自动注入 `userId`、`tenantId`。 -服务可以把数据库、缓存等依赖注册为就绪探针: +`WithLogger(true)` 开启 HTTP 访问日志。格式、级别、文件轮转、脱敏键和慢请求阈值均可配置,见 [日志](logx/README.md)。 -```go -checker, err := health.New(health.Probe{ - Name: "database", - Check: db.PingContext, -}) -if err != nil { - log.Fatal(err) -} -application.GET("/health/live", checker.LivenessHandler()) -application.GET("/health/ready", checker.ReadinessHandler()) +### 鉴权与业务错误 + +`.api` 可声明认证方式和关联中间件,例如把下面的声明放入契约,并在相应 `@server` 块中加上 `auth: UserAuth`: + +```text +auth UserAuth ( + type: bearer + middleware: Auth +) ``` -存活检查只确认进程在线;就绪检查会执行全部探针,任意依赖失败时返回 HTTP 503。 +生成器会创建 `internal/middleware/auth.go`,由项目实现凭据校验和身份注入。声明会生成路由绑定与 OpenAPI 的认证描述,**不会自动实现登录或授权规则**;未实现的 hook 返回 501。手写路由可使用 `auth.RequireAuth(authenticator)` 或自己的中间件。JWT、Session 和 Policy 按需选择。 -新项目脚手架会生成项目自己的 `internal/servicecontext`。它集中保存 Logger、数据库、缓存和外部客户端,Service 只依赖这个项目上下文,不把基础设施初始化散落到各个 Handler: +业务错误在 `.api` 中用 `error` 声明,通过 `@errors` 关联路由后,生成可调用的错误函数和 OpenAPI 中对应的 401、403、409 等响应。手写路由可以直接返回 `omni.NotFound`、`omni.Forbidden` 等错误。已有项目在自己的适配层转换原错误,内部原因和公开提示分开处理。 -```go -services, err := servicecontext.New(ctx, logger) -if err != nil { - return err -} -defer services.Close() -userService := service.NewUsers(services) -``` +### 数据库、存储及其他模块 -## 完整业务参考项目 +依赖在启动入口或项目自己的 `internal/servicecontext` 中创建,通过构造函数传给 Service/Repository,避免每个请求都新建连接池。只注册自己拥有的资源,例如 `app.OnShutdown(db.Close)`;关闭按注册的逆序执行。 -[`examples/reference`](examples/reference/README.md) 提供按 Handler / Service / Repository / Model 分层的用户 CRUD 示例。默认使用并发安全的内存存储,支持分页、邮箱唯一校验、结构化请求日志和统一错误,无需配置数据库。 +| 能力 | 包与接入说明 | +| --- | --- | +| 数据库连接池、事务、显式迁移 | `database`,见 [数据库](database/README.md);表结构与驱动由项目选择 | +| 本地文件、S3、R2、OSS、直传与临时下载 | `storage`,见 [文件存储](storage/README.md);业务依赖接口,装配时选择适配器 | +| 缓存、出站 HTTP、限流、幂等、后台任务 | `cache`、`httpc`、`middleware`、`task`,见 [基础模块](MODULES.md) | +| JWT/Session、策略、Redis 队列、审计、分页、观测 | 见 [扩展模块](ADVANCED.md);按需注入实现 | +| 存活与就绪检查 | `health`,见 [健康检查](health/README.md);数据库等依赖作为 readiness 探针 | -从远程版本生成并升级一个独立业务模块,可运行: +API 项目的数据库迁移需要单独维护并显式执行;启动不会自动改表。完整示例 [userService](examples/userService/README.md) 展示 MySQL、事务、鉴权、日志和文件导出,可参考依赖装配方式后接入自己的业务规则。 + +## 配置与运行 + +生成项目从 `etc/config.yaml` 读取配置,优先级为 **默认值 → 配置文件 → 环境变量**。密钥通过环境变量提供。手写入口需显式调用配置加载函数并传给 `omni.New(omni.WithConfig(settings))`;单独调用 `omni.New()` 不会自动读取环境变量。 + +| 环境变量 | 用途 | +| --- | --- | +| `GO_OMNI_HTTP_ADDR` | 监听地址;生成项目默认 `127.0.0.1:8080` | +| `GO_OMNI_TRANSPORT` | `nethttp`(默认)、`native`、`http2` | +| `GO_OMNI_TLS_CERT_FILE` / `GO_OMNI_TLS_KEY_FILE` | 成对设置 TLS 证书和私钥路径 | +| `GO_OMNI_SHUTDOWN_TIMEOUT` | 优雅退出等待时间,默认 `10s` | +| `GO_OMNI_ENABLE_LOGGER` / `GO_OMNI_LOG_LEVEL` | 访问日志开关、日志级别 | +| `GO_OMNI_TRUSTED_PROXIES` | 可解析转发地址头的可信代理 IP/CIDR,逗号分隔 | +| `GO_OMNI_DB_DSN` | 选择 MySQL 初始化时的数据库连接配置 | + +例如在生成项目中切换 native: ```sh -python3 scripts/verifyBusiness.py --version v0.6.0-preview +GO_OMNI_TRANSPORT=native go run ./cmd/server ``` -该验收会在临时目录中先固定 `v0.2.0`,再升级到 `v0.6.0-preview`,检查无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。 +手写入口可用 `omni.WithTransport(omni.TransportNative)`;TLS 配置为 `omni.WithTLS(certFile, keyFile)`。native 提供 HTTP/1.1、Keep-Alive、TLS、超时、连接/请求并发上限和对象复用;HTTP/2 是单独的实验适配器,必须配置 TLS。默认 nethttp 的 TLS 支持 HTTP/1.1 与 HTTP/2。 -标准库与 native 的并发、延迟和长连接内存对照见 [PERFORMANCE.md](PERFORMANCE.md)。`python3 scripts/verifyPerformance.py` 运行检查与 benchmark;增加 `--load --output /tmp/goOmniPerf.json` 可测量独立服务进程并保存报告。 +自研网络层通过 `RunWith(ctx, runner)` 接入监听与连接生命周期,通过 `NewWithRouter(router)` 替换路由;业务 Handler/Service 不需要导入具体适配器。细节与验收契约见 [网络适配层](transport/README.md)。 -HTTP/2 仍是显式 opt-in 实验适配器,不会改变 native 的 HTTP/1.1 默认路径。Linux TLS 长测使用 `python3 scripts/verifyHTTP2.py --duration 30m`,默认使用普通 Go 运行模式;`--race` 默认改为 60 秒,用于并发检查,其额外内存开销不能当成生产运行基线。 +`Run` 处理 SIGINT/SIGTERM;`RunContext` 接收调用方控制的退出信号。服务先停止接收新请求,等待活跃请求完成,再清理资源;超时会取消请求并关闭连接,业务代码仍需响应 context 取消。 -多连接混合场景使用 `python3 scripts/verifyHTTP2.py --scenario mixed --duration 30m`:8 条独立 TLS 连接,每条连接 14 个普通请求 worker、1 个慢上传 worker、1 个慢下载 worker,共 128 个 worker。上传为 256 KiB、每 4 KiB 等待 2 ms;下载为 8 MiB、每次最多读取 64 KiB 后等待 20 ms。日志分别记录三类成功数、传输字节数、每条连接的覆盖情况,以及停止并释放测试数据后两次 GC 的采样。 +## 日常开发与升级 -该脚本默认把测试容器限制为 2 核、2 GiB 内存(禁用 swap)、256 个进程/线程,整个命令最多运行 40 分钟,中断和超时会清理本次容器。这些限制可用 `--cpus`、`--memory-mb`、`--pids-limit`、`--wall-timeout` 调整;共享服务器应先用 `--duration 60s` 短测,再逐步延长。每 15 秒输出的堆、RSS 和 FD 统计包含同一进程内的客户端与服务端。 - -Linux amd64 已完成 30 分钟、单 TLS 连接、128 个 worker 的普通模式长测:58,102,476 次请求,0 失败、0 协议错误,采样 RSS 峰值约 22.6 MiB,连接/活动请求归零且 FD 回到基线。完整采样和原始日志见 [HTTP/2 长测报告](performance/http2SoakLinux.json) 与 [原始日志](performance/http2SoakLinux.txt),此前的 race 对照见 [短测记录](performance/http2SmokeLinux.json)。这项结果覆盖单连接复用。 - -多连接慢客户端场景也已完成 Linux amd64 的 30 分钟长测:8 条 TLS 连接、128 个 worker,53,079,128 次普通请求、106,971 次慢上传、5,588 次慢下载,共 53,191,687 次,0 失败与协议错误。采样 RSS 峰值约 116.6 MiB(包含客户端、服务端和下载缓冲);退出后连接/请求归零,goroutine 回到 2、FD 回到 6;释放测试数据并两次 GC 后存活堆约 450 KiB,RSS 不要求立即归还操作系统。完整 120 份采样见 [混合长测报告](performance/http2MixedSoakLinux.json) 和 [原始日志](performance/http2MixedSoakLinux.txt)。连接级窗口耗尽、WINDOW_UPDATE 恢复及零窗口写阻塞的关闭回收也已有独立测试;Nginx 的 HTTP/2 回源验收结果见下方,适配器保持实验状态。 - -`python3 scripts/verifyHTTP2Proxy.py --count 3` 验证真实 Nginx 双段 TLS/h2、回源连接复用、证书拒绝、超时/取消恢复与新旧实例的 GOAWAY 排空;增加 `--race` 可运行并发检查。Linux amd64、Nginx 1.30.5 的普通与 race 模式各 3 轮均已通过。编译和测试容器顺序运行,均限制为 2 核、2 GiB;测试容器禁用外部网络,全部请求在容器内完成。使用方法、报告及当前边界见 [HTTP/2 代理回源验收](transport/http2/PROXY.md)。 - -在框架根目录运行: +在已生成的 API 项目目录执行: ```sh -go run ./examples/reference +go-omni api validate +go-omni api inspect +go-omni api generate --dry-run +go-omni api generate +go-omni api generate --check +go test ./... ``` -默认地址为 `http://127.0.0.1:8080`。示例文档提供完整的 curl 操作流程。数据只保存在内存中,重启后清空。 +`Gen.go`、`docs/openapi.json` 和 manifest 由生成器管理,手动修改会报告冲突。Service、Repository、中间件实现、main、配置和依赖装配属于项目,重复生成保留手写内容;契约改变后仍需自行调整业务签名并通过编译。将 `.api`、生成文件和 `.goOmni/apiManifest.json` 一起提交,CI 可运行 `generate --check` 检查是否同步。 -框架的 `App` 也实现了标准 `http.Handler`,可以直接用于 `http.Server` 和 `httptest.NewServer`;业务路由可通过 `*omni.RouterGroup` 注册,无需导入网络库。 +升级时先安装目标版本的 CLI,再在业务 module 中 `go get` **同一版本**;API 项目随后执行上述生成与测试步骤,手写项目只需依赖升级及业务回归。`v0.8.0-preview` 的变化、迁移边界和验收记录见 [发布说明](RELEASE.md)。 -## 当前边界 +## 按需深入 -缓存、Redis 消息队列和进程内定时任务已作为可选模块提供。认证层提供 `Authenticator`、JWT/Session 适配器和 Policy 接口,登录流程与业务授权规则由项目实现。核心数据库层只依赖 `database/sql`,具体驱动、迁移和表结构由项目选择。 +| 需要了解 | 文档 | +| --- | --- | +| `.api` 类型、参数绑定、错误、鉴权和生成规则 | [API 开发指南](API.md) | +| 公共响应体、提示优先级、独立回调协议 | [响应参考](RESPONSE.md) | +| CLI 完整参数、本地框架开发和模块脚手架 | [CLI](cmd/go-omni/README.md) | +| 完整 API 业务项目 | [userService](examples/userService/README.md) | +| 手写接口分层示例 | [reference](examples/reference/README.md) | +| 框架成熟度、历史变更 | [路线图](ROADMAP.md)、[更新记录](CHANGELOG.md) | +| 网络性能、测试条件与适用范围 | [性能说明](PERFORMANCE.md)、[HTTP/2 验收](transport/native/HTTP2.md) | -当前成熟度与后续验收标准见 [ROADMAP.md](ROADMAP.md),版本变更见 [CHANGELOG.md](CHANGELOG.md)。 - -`v0.8.0-preview` 新增 [公共响应模板](RESPONSE.md):在 `.api` 中定义一次返回公共体,分组与接口引用,运行时和 OpenAPI 共用。支持数字错误码、嵌套结构、原始 JSON,以及独立的文本/XML回调编码器。源码开发可通过 `--framework-dir` 显式引用本地框架。 +当前版本可用于固定版本接入与业务验证,仍标记为 preview。`.api` 借鉴 go-zero 的接口开发方式,支持范围以本仓库文档为准,不是完整 goctl 语法兼容层;HTTP/2 保持实验状态,OSS/R2 云账号实网验收不属于本次发布范围。 diff --git a/RELEASE.md b/RELEASE.md index 01eb403..1dc11d2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -6,16 +6,21 @@ ## 安装与升级 +首次接入请从 [README 接入指南](README.md) 开始,按新项目或已有项目选择步骤。下面是已接入项目的版本升级命令: + ```sh GOPRIVATE=git.yuntuo.hk go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.8.0-preview -# 已有业务项目中执行: +# 在业务项目 go.mod 所在目录执行: GOPRIVATE=git.yuntuo.hk go get git.yuntuo.hk/k2safe/go-omni@v0.8.0-preview +# 仅在已经由 .api 生成、具有 .goOmni/apiManifest.json 的项目中执行: go-omni api generate go test ./... ``` -新项目可以直接 `go-omni api new --module example.com/demo ./demo`,或用 `go-omni new --api service.api --module example.com/demo ./demo`。远程安装的 CLI 会固定生成项目的框架版本,无需本地 replace。需要仓库权限时使用自己的 Git 凭据管理器,不把凭据写入 module path 或 go.mod。 +手写接口项目跳过 `api generate`,仍需执行业务测试。生成器不会自动接管其他框架创建的已有项目。 + +新项目可以直接 `go-omni api new --module example.com/demo ./demo`,或用 `go-omni new --api service.api --module example.com/demo ./demo`。在框架源码目录之外运行,远程安装的 CLI 会固定生成项目的框架版本,无需本地 replace;也可显式传 `--framework-version v0.8.0-preview` 固定远程依赖。需要仓库权限时使用自己的 Git 凭据管理器,不把凭据写入 module path 或 go.mod。 ## 本版功能 diff --git a/cmd/go-omni/README.md b/cmd/go-omni/README.md index c213b79..412c55f 100644 --- a/cmd/go-omni/README.md +++ b/cmd/go-omni/README.md @@ -1,5 +1,7 @@ # go-omni CLI +首次接入见 [README](../../README.md),无需克隆框架源码。本页说明命令参数;下方 `go run ./cmd/go-omni` 示例在框架仓库中执行,并显式引用本地源码。 + ## API 契约驱动项目 ```sh @@ -73,8 +75,8 @@ Handler / Service / Repository / Model 与模块注册入口。模块名为 lowe 从远程安装,生成一个带 Handler / Service / Repository / Model 分层的服务骨架: ```sh -go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.7.0-preview.1 -go-omni new --module example.com/billing ./billing +GOPRIVATE=git.yuntuo.hk go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.8.0-preview +go-omni new --framework-version v0.8.0-preview --module example.com/billing ./billing cd billing go mod tidy go run ./cmd/server diff --git a/logx/README.md b/logx/README.md index ec15cfc..d890f7e 100644 --- a/logx/README.md +++ b/logx/README.md @@ -30,7 +30,7 @@ if err != nil { return err } defer closeLog() ``` -也可用 `omni.WithLogConfig(...)` 交给应用运行生命周期管理。子日志共用输出、级别和关闭状态。独立 `logx.New` 返回的实例由创建者负责关闭。 +`omni.WithLogConfig(...)` 只保存日志配置,`omni.New()` 不会因此创建或关闭 Logger。手写入口需要像上例一样调用 `logx.Setup` 并负责关闭,或使用 `logx.New`、`logx.SetDefault` 和 `app.OnShutdown(logger.Close)` 显式装配。生成的 API 项目已在入口完成这些步骤。子日志共用输出、级别和关闭状态;独立 `logx.New` 返回的实例由创建者负责关闭。 轮转显式开启;三个数值为 0 时分别取 100 MiB、10 份、7 天。超过大小后轮转,旧文件按份数/年龄清理,可 gzip 压缩;保留天数不表示每天定时切割。压缩和历史清理由底层异步执行,不承诺 Close 等待压缩完成。轮转使用 [lumberjack v2](https://github.com/natefinch/lumberjack/tree/v2.2.1),**一个日志文件只能由一个进程拥有**。容器通常直接用 stdout/stderr,让运行平台管理日志文件。