feat: add typed error message templates and stable replay snapshots

This commit is contained in:
2026-09-24 15:05:08 +08:00
parent 2041e7f661
commit 6e3f025aad
32 changed files with 1570 additions and 99 deletions

View File

@@ -110,6 +110,68 @@ service account-api {
生成路由的目录和响应由 `.api` 管理;YAML 或 App 默认策略不覆盖它。手写路由可在 main 用 `omni.WithErrors(apierrors.Catalog())` 并选择兼容的 `WithResponse`。新项目和重复生成都会自动绑定路由目录,无需手动修改 main;已有手写文件保持不动。
## 提示模板参数(v0.11.0-preview)
模板属于错误目录,业务只传具体值。参数用**有序列表**声明,顺序就是生成函数的参数顺序;每个参数均为必传,`example` 只用于文档。
```text
errorCatalog CommonErrors (
defaultStatus: 200
errors: {
"TextTooLong": {
"key": "TEXT_TOO_LONG",
"code": 400002,
"message": "{label}不能超过{max}个字符",
"params": [
{"name": "label", "type": "string", "example": "企业名称"},
{"name": "max", "type": "int", "example": 100}
]
}
}
)
```
与数字码匹配的公共响应及路由声明见完整 [errorParams.api](../examples/contracts/errorParams.api)。生成后:
```go
apierrors.TextTooLong("企业名称", 100) // Message:企业名称不能超过100个字符
apierrors.TextTooLong("备注", 500) // Message:备注不能超过500个字符
apierrors.UsernameInvalid() // 原有固定错误保持无参数
```
生成函数在返回前完成解析,非 HTTP Service、任务或 CLI 同样能直接读取完整 Message。调用 `.WithCause(err)` 保留内部原因及 `errors.Is/As` 错误链,不自动把原因加入提示。每次返回新对象,不能把可变错误保存成共享单例。
运行时入口使用同一个解析器:
```go
err := omnierrors.ByCode("TEXT_TOO_LONG").WithParams(map[string]any{
"label": "企业名称",
"max": 100,
})
// HTTP:将 err 返回,当前 App/路由目录负责解析。
// 非 HTTP:使用项目自己的目录显式解析。
registry := omnierrors.MustRegistry(apierrors.Catalog())
resolved := registry.Resolve(err)
fmt.Println(resolved.Message) // 企业名称不能超过100个字符
```
Go 配置中的 `Definition.Params` 是 `[]omnierrors.Parameter`,JSON/YAML 使用相同的 `params` 数组。目录在初始化时复制并编译模板;`WithParams` 返回新错误并复制参数,之后修改原 map 不会改变该错误。重复解析、路由策略和 Context 缓冲克隆保留参数。
规则如下:
- 参数名使用 lowerCamelCase,并且不是 Go 关键字。顺序是调用 API 的一部分,重排或变更类型需要同步调整调用方。
- 支持 `string`、`bool`、`int`、`int32`、`int64`、`float64`。运行时允许整数类型之间按声明范围归一化,拒绝将字符串或浮点数隐式转成整数;float64 要求有限数值。`int` 范围随 Go 目标平台变化,需要固定位宽时选 int32/int64。
- 只接受标量,不接受 nil、切片、map、指针、任意对象或 Stringer。通过 JSON 构建运行时参数时使用 `Decoder.UseNumber()`,避免丢失大整数精度。
- 模板使用 `{name}`;同一参数可重复出现。`{{` / `}}` 表示字面花括号。插入值不会再次解析,即使 label 本身包含 `{max}`,也只是普通文本。
- **省略 params** 表示旧固定提示,字面花括号不变。不接受空 params 数组;重复参数、未声明占位符、未使用参数、不匹配的花括号、非法类型及错误的 example 会在目录加载/契约校验时失败。
- 运行时缺参、多传参数、类型或范围错误转为系统错误兜底;不会将原始模板或参数值作为诊断发给客户端。内部 cause 中可通过 `errors.As` 获取 `*omnierrors.ParamError`,访问日志带 `errorParameter` 诊断,不自动记录参数值。
- 项目可以配置固定的 INTERNAL_ERROR 兜底;若它自身也需要参数且无法解析,则回退框架固定 500,避免递归失败。
- 模板错误的提示只放目录中;同标识再配置旧 `messages` 会被拒绝。响应字段、HTTP 状态、`data:null` 仍由项目策略决定,例如 `dataMode: always` 保留空 data。
OpenAPI 的 `x-omni-errors` 保留模板和有序参数定义,响应 examples 使用同一个解析器生成具体文案。未提供 example 时使用稳定样例(string 使用参数名,bool 使用 false,数值使用 1);这些样例不充当运行时默认值,也不是新增 HTTP 请求参数。
幂等重放保存当次**已解析的公开错误快照**,保留原响应与失败信息。目录更新后,仍在有效期内的重放不会被新文案覆盖;快照不持久化原始参数或内部 cause。整数业务码以不受 JSON 中间层浮点转换影响的方式保存。旧缓存仅保留已有错误标识,不能补推当时缺失的提示与业务码。共享存储的错误快照另受 MaxResponseBytes 限额约束,超限保留不可重放记录,避免重复执行业务。
## 特殊协议与失败信息
`response.WithErrors(catalog, encoder)` / `MustWithErrors` 可绑定路由专用策略,再通过 `HandleWithResponse` 注册。内置模板校验目录码类型与旧映射;自定义 Encoder 自行决定最终 HTTP 状态、媒体类型和字段结构,可将失败转换成第三方要求的文本/XML 回执。一个 Encoder 作用域不叠加不同目录;外层空目录或相同目录不会覆盖已有目录。
@@ -123,11 +185,11 @@ Encoder 的 `response.Result` 包含逻辑 `Code`、对外 `BusinessCode`、公
## 升级
```sh
go get git.yuntuo.hk/k2safe/go-omni@v0.10.0-preview
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.10.0-preview
go get git.yuntuo.hk/k2safe/go-omni@v0.11.0-preview
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.11.0-preview
go-omni api generate --dry-run
go-omni api generate
go test ./...
```
先升级框架依赖,再用同版 CLI 生成;新生成代码最低依赖 v0.10.0-preview。原来的错误构造函数、旧 `error (...)`、Codes/Messages 与已生成代码可继续运行。把业务中散落的状态/提示逐步替换为生成函数属于项目迁移工作,框架不会自动改写业务逻辑。
先升级框架依赖,再用同版 CLI 生成;新生成代码最低依赖 v0.11.0-preview。原来的错误构造函数、旧 `error (...)`、Codes/Messages 与已生成代码可继续运行。把业务中散落的状态/提示逐步替换为生成函数属于项目迁移工作,框架不会自动改写业务逻辑。