From 9019798382959ab87ee6c594aa92b106156aefa9 Mon Sep 17 00:00:00 2001 From: chiyi Date: Fri, 21 Aug 2026 22:29:41 +0800 Subject: [PATCH] v1.3.3: add SetBodyLimit for large custom-route request bodies (multipart uploads) --- docs/README.zh.md | 154 ++++++++++++++++++++++++++++++++++++++++++++++ fun.go | 18 +++++- 2 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 docs/README.zh.md diff --git a/docs/README.zh.md b/docs/README.zh.md new file mode 100644 index 0000000..f7cabe5 --- /dev/null +++ b/docs/README.zh.md @@ -0,0 +1,154 @@ +# fun 框架(github.com/cyi-cc/fun)使用文档 + +> 适用版本:**v1.3.2**(当前最新发布)。基于 fasthttp 的单端点 RPC 框架, +> 自带依赖注入、Guard 鉴权、NDJSON 流式响应、自定义路由与 TypeScript 客户端生成。 + +## 版本沿革 + +| 版本 | 要点 | +|---|---| +| v1.1.0 | BindRoute 自定义 GET/POST 路由(回调、健康检查) | +| v1.3.0 | BindRoute 通配符路由 `/prefix/*`,`RouteCtx.Wildcard` 取剩余路径 | +| v1.3.1 | TS 客户端可靠性:所有失败统一归一为 Result 并经过响应拦截器 | +| v1.3.2 | 每请求上下文(request/stream options + `state`)与免基础设施的生成期注册 `BindServiceForGen` | + +## 1. 启动与服务注册 + +```go +func main() { + f := fun.GetFun() + f.BindService(&UserSvc{}, &AuthGuard{}) // 服务级 Guard 可选 + f.BindGuard(&LogGuard{}) // 全局 Guard + + cfg := fun.Wired[config.Config]() // 创建/获取单例(触发 DI) + go f.Start(cfg.ListenPort()) // fasthttp 监听,RPC 只响应 POST /cell +} +``` + +- 服务结构体嵌入 `fun.Ctx` + 依赖字段(指针结构体字段自动装配)。 +- **每请求新建服务实例**并注入依赖,服务内不放共享状态。 +- 方法签名四种:`() error`、`(dto) (T, error)`、`(dto) (*fun.Stream, error)`、 + `(dto) (T, *fun.Stream, error)`(首条消息 T + 后续流)。 +- 只有导出方法成为端点,注册名 `服务名.方法名`。 + +## 2. DTO 规则(违反即注册期 panic) + +- 允许:定宽整型(int8…int64、uint8…uint64)、string、bool、具名 struct、slice、指针。 +- **不支持**:普通 `int`/`uint`、float、map、any/interface、匿名结构体、私有字段。 +- 非指针且非 slice 字段必传且非 null;可空字段一律 `*T`;小数用字符串传。 +- 枚举:`uint8` 底线 + `Names() []string`(可选 `DisplayNames()`)。 +- 响应所有键递归转首字母小写,前端直接 camelCase 取值。 + +## 3. 线协议与 Result + +请求:`POST /cell`,body `{"serviceName","methodName","data","state"}`。 + +```ts +export type result = { + id?: string; code?: number; data?: T; msg?: string; status: number +} +``` + +- `status`:`0` 成功;`1` 框架/协议/基础设施失败;`2` 业务失败(`fun.Error(code,msg)`); + `4` 外部请求失败或调用方取消;`5` 明确的外部超时失败。 +- 业务错误:`return nil, fun.Error(4001, "登录失败")` —— code/msg 原样透传。 +- 成功空 slice 序列化为 `[]`;`data` 为 nil 时整个字段省略。 +- `Ctx.State`(map[string]string)请求往返透传;`Ctx.Ip` 已解析客户端 IP。 + +## 4. Guard(v1.3.2 无变化,推荐用法见 vividai) + +```go +func (g *AuthGuard) Guard(ctx fun.Ctx) { /* 校验失败 panic(fun.Error(...)) */ } +f.BindService(&AdminSvc{}, &AuthGuard{}) +``` + +Guard 也是 Box,字段自动注入;panic 被框架兜底转错误响应。 +vividai 的用法:**显式端点策略表**(缺省拒绝)+ Guard 从 HttpOnly Cookie 读会话, +并把校验结果经 `RequestCtx.SetUserValue` 传给服务层做对象级授权。 + +## 5. 自定义路由(v1.3.0+) + +```go +f.BindRoute("GET", "/image/*", func(c *fun.RouteCtx) error { + key := c.Wildcard // /image/ 之后的剩余路径 + c.RequestCtx.WriteString("…") // 纯文本直写;返回 nil 框架不再写 + return fun.Error(4001, "…") // 或统一 Result 错误 +}) +``` + +- 精确路由优先于通配符;`/cell` 保留;方法大小写不敏感。 +- 查询参数与 form 表单合并进 `c.Param(name)`;multipart 不支持(转 base64 走 /cell)。 + +## 6. 流式响应(NDJSON) + +```go +st := &fun.Stream{} +go func() { + for _, chunk := range chunks { + if st.Send(chunk) != nil { return } // 连接断开 + } + st.Close() // 必须关闭 +}() +return st, nil +``` + +`Content-Type: application/x-ndjson`,每行一个 JSON;合法零消息流正常结束; +业务出错在建流前返回普通 Result;`OnClose` 注册清理回调。 + +## 7. TS 客户端生成与请求上下文(v1.3.2 核心) + +```go +f := fun.GetFun() +f.BindServiceForGen(&UserSvc{}) // 生成期专用:只反射注册方法,不装配任何基础设施 +fun.SetOutput("./frontend/src/api") +fun.GenCode(fun.GenTs{}) +``` + +- `BindServiceForGen` 不触发 Box 装配,生成命令**不需要数据库/Redis 在运行**。 +- 生成确定性:service/method/imports 全部源端排序,重复生成字节一致。 +- 产物:`client.ts`(Client + `result`)、每服务一个 `.ts`、DTO/View 类型、 + `fun.ts`(`api.create(url)` 聚合入口,服务属性首字母小写)。 + +### 每调用选项(v1.3.2) + +```ts +export type RequestOptions = { signal?: AbortSignal; state?: Record } +export type StreamOptions = { signal?: AbortSignal; state?: Record } + +const r = await c.userSvc.profile({ signal: ctrl.signal }) +c.chatSvc.chat(dto, msg => {...}, { signal: ctrl.signal }) +``` + +- 调用方可传 `AbortSignal`;**框架自身不设任何请求/连接/空闲超时定时器**, + 超时由调用方或网关(nginx)决定,框架只负责把失败归一为 Result。 +- `state` 为每请求字符串字典:请求拦截器可写入(如会话纪元、请求标识), + 响应拦截器经 `context.requestState` 只读快照取回。 + +### 拦截器(v1.3.2 四参上下文形态) + +```ts +c.addRequestInterceptor((svc, m, state) => { state.epoch = myEpoch() }) + +c.addResponseInterceptor((svc, m, result, context) => { + // context.requestState: Readonly> + // context.response?: Response —— 原生 Response(头、状态码可读) +}) +``` + +旧三参签名仍兼容。所有失败(网络、HTTP、HTML、非法 JSON、取消、拦截器异常) +统一归一为 Result 并**必经响应拦截器**,不存在绕过拦截器的错误路径。 + +## 8. 依赖注入(box.go) + +- `fun.Wired[T]()`:按 `*T` 建单例;先注入 `fun:"auto"` 字段(缺则递归创建), + 再调 `New()`(无参;连接类资源在此初始化,失败可 log.Fatalf)。 +- 启动顺序:先 `Wired` 基础配置/平台单例,再 `BindService`。 + +## 9. 常见坑 + +- DTO 用普通 `int`/float/map → 注册期 panic;用 int64/字符串/指针。 +- 非指针字段漏传 → 运行期 "must be a pointer or have a corresponding field"。 +- 流式忘记 `Close()` → 客户端挂起;连接断开后 `Send` 返回 error,循环须检查。 +- 方法首字母小写 = 不注册;客户端报 method not found。 +- vite 代理需重写前缀:`/api/cell → /cell`。 +- 生成用 `BindService`(而非 `BindServiceForGen`)会把基础设施拉起来 —— 生成命令请用后者。 diff --git a/fun.go b/fun.go index b11dc50..a3136e8 100644 --- a/fun.go +++ b/fun.go @@ -15,6 +15,16 @@ type Fun struct { boxes *sync.Map // 依赖容器:reflect.Type → reflect.Value guards []*any // 全局 Guard serviceGuards map[string][]*any // 服务级 Guard,按服务名 + bodyLimit int // 请求体上限(字节);0 = fasthttp 默认 4MB +} + +// SetBodyLimit 设置请求体上限(字节),须在 Start 前调用。 +// multipart 上传等大请求体的自定义路由需要时设置;0 或负数恢复默认。 +func (f *Fun) SetBodyLimit(n int) { + if n < 0 { + n = 0 + } + f.bodyLimit = n } // wildcardRoute 通配符路由(BindRoute path 以 "/*" 结尾注册): @@ -172,9 +182,11 @@ func (f *Fun) callGuard(c *Ctx, serviceName string) { } func (f *Fun) Start(port uint16) { - addr := fmt.Sprintf(":%d", port) - err := fasthttp.ListenAndServe(addr, f.handle) - if err != nil { + server := &fasthttp.Server{Handler: f.handle} + if f.bodyLimit > 0 { + server.MaxRequestBodySize = f.bodyLimit + } + if err := server.ListenAndServe(fmt.Sprintf(":%d", port)); err != nil { panic(err.Error()) } }