Compare commits
3 Commits
76c7592f26
...
0096775d31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0096775d31 | ||
|
|
82c94d5a29 | ||
|
|
f2a8b9876e |
@@ -3,6 +3,7 @@ package route
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/samber/lo"
|
||||
@@ -15,35 +16,40 @@ type RenderBuildOpts struct {
|
||||
}
|
||||
|
||||
func buildRenderData(opts RenderBuildOpts) (RenderData, error) {
|
||||
rd := RenderData{
|
||||
PackageName: opts.PackageName,
|
||||
ProjectPackage: opts.ProjectPackage,
|
||||
Imports: []string{},
|
||||
Controllers: []string{},
|
||||
Routes: make(map[string][]Router),
|
||||
RouteGroups: []string{},
|
||||
}
|
||||
rd := RenderData{
|
||||
PackageName: opts.PackageName,
|
||||
ProjectPackage: opts.ProjectPackage,
|
||||
Imports: []string{},
|
||||
Controllers: []string{},
|
||||
Routes: make(map[string][]Router),
|
||||
RouteGroups: []string{},
|
||||
}
|
||||
|
||||
imports := []string{}
|
||||
controllers := []string{}
|
||||
imports := []string{}
|
||||
controllers := []string{}
|
||||
// Track if any param uses model lookup, which requires the field package.
|
||||
needsFieldImport := false
|
||||
|
||||
for _, route := range opts.Routes {
|
||||
imports = append(imports, route.Imports...)
|
||||
controllers = append(controllers, fmt.Sprintf("%s *%s", strcase.ToLowerCamel(route.Name), route.Name))
|
||||
|
||||
for _, action := range route.Actions {
|
||||
funcName := fmt.Sprintf("Func%d", len(action.Params))
|
||||
if action.HasData {
|
||||
funcName = "Data" + funcName
|
||||
}
|
||||
for _, action := range route.Actions {
|
||||
funcName := fmt.Sprintf("Func%d", len(action.Params))
|
||||
if action.HasData {
|
||||
funcName = "Data" + funcName
|
||||
}
|
||||
|
||||
params := lo.FilterMap(action.Params, func(item ParamDefinition, _ int) (string, bool) {
|
||||
tok := buildParamToken(item)
|
||||
if tok == "" {
|
||||
return "", false
|
||||
}
|
||||
return tok, true
|
||||
})
|
||||
params := lo.FilterMap(action.Params, func(item ParamDefinition, _ int) (string, bool) {
|
||||
tok := buildParamToken(item)
|
||||
if tok == "" {
|
||||
return "", false
|
||||
}
|
||||
if item.Model != "" {
|
||||
needsFieldImport = true
|
||||
}
|
||||
return tok, true
|
||||
})
|
||||
|
||||
rd.Routes[route.Name] = append(rd.Routes[route.Name], Router{
|
||||
Method: strcase.ToCamel(action.Method),
|
||||
@@ -54,13 +60,18 @@ func buildRenderData(opts RenderBuildOpts) (RenderData, error) {
|
||||
Params: params,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// de-dup and sort imports/controllers for stable output
|
||||
rd.Imports = lo.Uniq(imports)
|
||||
sort.Strings(rd.Imports)
|
||||
rd.Controllers = lo.Uniq(controllers)
|
||||
sort.Strings(rd.Controllers)
|
||||
// Add field import if any model lookups are used
|
||||
if needsFieldImport {
|
||||
imports = append(imports, `field "go.ipao.vip/gen/field"`)
|
||||
}
|
||||
|
||||
// de-dup and sort imports/controllers for stable output
|
||||
rd.Imports = lo.Uniq(imports)
|
||||
sort.Strings(rd.Imports)
|
||||
rd.Controllers = lo.Uniq(controllers)
|
||||
sort.Strings(rd.Controllers)
|
||||
|
||||
// stable order for route groups and entries
|
||||
for k := range rd.Routes {
|
||||
@@ -85,41 +96,51 @@ func buildRenderData(opts RenderBuildOpts) (RenderData, error) {
|
||||
}
|
||||
|
||||
func buildParamToken(item ParamDefinition) string {
|
||||
key := item.Name
|
||||
if item.Key != "" {
|
||||
key = item.Key
|
||||
}
|
||||
key := item.Name
|
||||
if item.Key != "" {
|
||||
key = item.Key
|
||||
}
|
||||
|
||||
switch item.Position {
|
||||
case PositionQuery:
|
||||
return fmt.Sprintf(`Query%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
|
||||
case PositionHeader:
|
||||
return fmt.Sprintf(`Header[%s]("%s")`, item.Type, key)
|
||||
case PositionFile:
|
||||
return fmt.Sprintf(`File[multipart.FileHeader]("%s")`, key)
|
||||
case PositionCookie:
|
||||
if item.Type == "string" {
|
||||
return fmt.Sprintf(`CookieParam("%s")`, key)
|
||||
}
|
||||
return fmt.Sprintf(`Cookie[%s]("%s")`, item.Type, key)
|
||||
case PositionBody:
|
||||
return fmt.Sprintf(`Body[%s]("%s")`, item.Type, key)
|
||||
case PositionPath:
|
||||
// If a model field is specified, generate a model-lookup binder from path value.
|
||||
if item.ModelField != "" || item.Model != "" {
|
||||
field := item.ModelField
|
||||
if field == "" {
|
||||
field = "id"
|
||||
}
|
||||
// PathModel is expected to resolve the path param to the specified model by field.
|
||||
// Example: PathModel[models.User]("id", "user_id")
|
||||
return fmt.Sprintf(`PathModel[%s]("%s", "%s")`, item.Type, field, key)
|
||||
}
|
||||
return fmt.Sprintf(`Path%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
|
||||
case PositionLocal:
|
||||
return fmt.Sprintf(`Local[%s]("%s")`, item.Type, key)
|
||||
}
|
||||
return ""
|
||||
switch item.Position {
|
||||
case PositionQuery:
|
||||
return fmt.Sprintf(`Query%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
|
||||
case PositionHeader:
|
||||
return fmt.Sprintf(`Header[%s]("%s")`, item.Type, key)
|
||||
case PositionFile:
|
||||
return fmt.Sprintf(`File[multipart.FileHeader]("%s")`, key)
|
||||
case PositionCookie:
|
||||
if item.Type == "string" {
|
||||
return fmt.Sprintf(`CookieParam("%s")`, key)
|
||||
}
|
||||
return fmt.Sprintf(`Cookie[%s]("%s")`, item.Type, key)
|
||||
case PositionBody:
|
||||
return fmt.Sprintf(`Body[%s]("%s")`, item.Type, key)
|
||||
case PositionPath:
|
||||
// If a model field is specified, generate a model-lookup binder from path value.
|
||||
if item.Model != "" {
|
||||
field := "id"
|
||||
fieldType := "int"
|
||||
if strings.Contains(item.Model, ":") {
|
||||
parts := strings.SplitN(item.Model, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
field = parts[0]
|
||||
fieldType = parts[1]
|
||||
}
|
||||
} else {
|
||||
field = item.Model
|
||||
}
|
||||
|
||||
tpl := `func(ctx fiber.Ctx) (*%s, error) {
|
||||
v := fiber.Params[%s](ctx, "%s")
|
||||
return %sQuery.WithContext(ctx).Where(field.NewUnsafeFieldRaw("%s = ?", v)).First()
|
||||
}`
|
||||
return fmt.Sprintf(tpl, item.Type, fieldType, key, item.Type, field)
|
||||
}
|
||||
return fmt.Sprintf(`Path%s[%s]("%s")`, scalarSuffix(item.Type), item.Type, key)
|
||||
case PositionLocal:
|
||||
return fmt.Sprintf(`Local[%s]("%s")`, item.Type, key)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func scalarSuffix(t string) string {
|
||||
|
||||
@@ -29,14 +29,11 @@ type ActionDefinition struct {
|
||||
}
|
||||
|
||||
type ParamDefinition struct {
|
||||
Name string
|
||||
Type string
|
||||
Key string
|
||||
Model string
|
||||
// ModelField is the field/column name used to lookup the model when Model is set.
|
||||
// Example: `@Bind user path key(id) model(database/models.User:id)` -> Model=database/models.User, ModelField=id
|
||||
ModelField string
|
||||
Position Position
|
||||
Name string
|
||||
Type string
|
||||
Key string
|
||||
Model string
|
||||
Position Position
|
||||
}
|
||||
|
||||
type Position string
|
||||
@@ -141,15 +138,19 @@ func ParseFile(file string) []RouteDefinition {
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "@Bind") {
|
||||
//@Bind name [uri|query|path|body|header|cookie] [key()] [table()] [model(<pkg>.<Type>[:<field>])]
|
||||
bindParams = append(bindParams, parseRouteBind(line))
|
||||
}
|
||||
//@Bind name [uri|query|path|body|header|cookie] [key()] [table()] [model(<pkg>.<Type>[:<field>])]
|
||||
bindParams = append(bindParams, parseRouteBind(line))
|
||||
}
|
||||
}
|
||||
|
||||
if path == "" || method == "" {
|
||||
continue
|
||||
}
|
||||
log.WithField("file", file).WithField("action", decl.Name.Name).WithField("path", path).WithField("method", method).Info("get router")
|
||||
log.WithField("file", file).
|
||||
WithField("action", decl.Name.Name).
|
||||
WithField("path", path).
|
||||
WithField("method", method).
|
||||
Info("get router")
|
||||
|
||||
// 拿参数列表去, 忽略 context.Context 参数
|
||||
orderBindParams := []ParamDefinition{}
|
||||
@@ -239,44 +240,32 @@ func parseRouteComment(line string) (string, string, error) {
|
||||
}
|
||||
|
||||
func parseRouteBind(bind string) ParamDefinition {
|
||||
var param ParamDefinition
|
||||
parts := strings.FieldsFunc(bind, func(r rune) bool {
|
||||
return r == ' ' || r == '(' || r == ')' || r == '\t'
|
||||
})
|
||||
parts = lo.Filter(parts, func(item string, idx int) bool {
|
||||
return item != ""
|
||||
})
|
||||
var param ParamDefinition
|
||||
parts := strings.FieldsFunc(bind, func(r rune) bool {
|
||||
return r == ' ' || r == '(' || r == ')' || r == '\t'
|
||||
})
|
||||
parts = lo.Filter(parts, func(item string, idx int) bool {
|
||||
return item != ""
|
||||
})
|
||||
|
||||
for i, part := range parts {
|
||||
switch part {
|
||||
case "@Bind":
|
||||
param.Name = parts[i+1]
|
||||
param.Position = positionFromString(parts[i+2])
|
||||
case "key":
|
||||
param.Key = parts[i+1]
|
||||
case "model":
|
||||
// Supported formats:
|
||||
// - model(field) -> only specify model field/column; model type inferred from parameter
|
||||
// - model(pkg/path.Type) -> type hint (optional); default field will be used later
|
||||
// - model(pkg/path.Type:id) or model(pkg/path.Type#id) -> type + field
|
||||
mv := parts[i+1]
|
||||
// if mv contains no dot, treat as field name directly
|
||||
if !strings.Contains(mv, ".") && !strings.Contains(mv, "/") {
|
||||
param.ModelField = mv
|
||||
break
|
||||
}
|
||||
// otherwise try type[:field]
|
||||
fieldSep := ":"
|
||||
if strings.Contains(mv, "#") {
|
||||
fieldSep = "#"
|
||||
}
|
||||
if idx := strings.LastIndex(mv, fieldSep); idx > 0 && idx < len(mv)-1 {
|
||||
param.Model = mv[:idx]
|
||||
param.ModelField = mv[idx+1:]
|
||||
} else {
|
||||
param.Model = mv
|
||||
}
|
||||
}
|
||||
}
|
||||
return param
|
||||
for i, part := range parts {
|
||||
switch part {
|
||||
case "@Bind":
|
||||
param.Name = parts[i+1]
|
||||
param.Position = positionFromString(parts[i+2])
|
||||
case "key":
|
||||
param.Key = parts[i+1]
|
||||
case "model":
|
||||
// Supported formats:
|
||||
// - model(field:field_type) -> only specify model field/column;
|
||||
mv := parts[i+1]
|
||||
// if mv contains no dot, treat as field name directly
|
||||
if mv == "" {
|
||||
param.Model = "id"
|
||||
break
|
||||
}
|
||||
param.Model = mv
|
||||
}
|
||||
}
|
||||
return param
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.ipao.vip/atomctl/v2/pkg/utils/gomod"
|
||||
)
|
||||
|
||||
// Test that @Bind with model(field) on a path parameter generates PathModel[T](field, key)
|
||||
func Test_PathModelBind_FromRouteComments(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type User struct{}
|
||||
|
||||
type Demo struct{}
|
||||
|
||||
// @Router /users/:id [get]
|
||||
// @Bind user path key(id) model(id)
|
||||
func (d *Demo) Show(ctx context.Context, user *User) (*User, error) {
|
||||
return nil, nil
|
||||
}`
|
||||
|
||||
// minimal go.mod so gomod.GetPackageModuleName works without panic
|
||||
gomodPath := filepath.Join(dir, "go.mod")
|
||||
goModContent := "module example.com/test\n\ngo 1.23\n"
|
||||
if err := os.WriteFile(gomodPath, []byte(goModContent), 0o644); err != nil {
|
||||
t.Fatalf("write go.mod: %v", err)
|
||||
}
|
||||
|
||||
if err := gomod.Parse(gomodPath); err != nil {
|
||||
t.Fatalf("gomod.Parse error: %v", err)
|
||||
}
|
||||
|
||||
file := filepath.Join(dir, "demo.go")
|
||||
if err := os.WriteFile(file, []byte(src), 0o644); err != nil {
|
||||
t.Fatalf("write file: %v", err)
|
||||
}
|
||||
|
||||
defs := ParseFile(file)
|
||||
if len(defs) != 1 {
|
||||
t.Fatalf("expected 1 route definition, got %d", len(defs))
|
||||
}
|
||||
if len(defs[0].Actions) != 1 {
|
||||
t.Fatalf("expected 1 action, got %d", len(defs[0].Actions))
|
||||
}
|
||||
act := defs[0].Actions[0]
|
||||
if len(act.Params) != 1 {
|
||||
t.Fatalf("expected 1 param, got %d", len(act.Params))
|
||||
}
|
||||
p := act.Params[0]
|
||||
if p.Position != PositionPath {
|
||||
t.Fatalf("expected path position, got %s", p.Position)
|
||||
}
|
||||
if p.Key != "id" {
|
||||
t.Fatalf("expected key=id, got %s", p.Key)
|
||||
}
|
||||
if p.ModelField != "id" {
|
||||
t.Fatalf("expected ModelField=id, got %s", p.ModelField)
|
||||
}
|
||||
if p.Type != "User" { // pointer should be trimmed for non-local
|
||||
t.Fatalf("expected Type=User, got %s", p.Type)
|
||||
}
|
||||
|
||||
// Build render data and check binder token
|
||||
rd, err := buildRenderData(RenderBuildOpts{
|
||||
PackageName: "v1",
|
||||
ProjectPackage: "example.com/test",
|
||||
Routes: defs,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildRenderData error: %v", err)
|
||||
}
|
||||
// Render to text and assert PathModel usage
|
||||
out, err := renderTemplate(rd)
|
||||
if err != nil {
|
||||
t.Fatalf("renderTemplate error: %v", err)
|
||||
}
|
||||
got := string(out)
|
||||
if !strings.Contains(got, "PathModel[User](\"id\", \"id\")") {
|
||||
t.Fatalf("expected generated code to contain PathModel[User](\"id\", \"id\"), got:\n%s", got)
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,87 @@
|
||||
## 路由定义
|
||||
## 路由生成(gen route)
|
||||
|
||||
路由由控制器方法的注释进行声明式定义,解析器会从 Go AST 中读取注释、方法签名与参数列表,生成对应的路由注册与参数绑定代码。
|
||||
通过在控制器方法上编写注释,解析器从 Go AST 中读取注释、方法签名与参数列表,自动生成路由注册与参数绑定代码。
|
||||
|
||||
- 关键标签:`@Router` 定义路径与方法;`@Bind` 定义方法参数与其来源位置及键名。
|
||||
- 生成行为:根据注释规则生成 `router.<METHOD>(path, FuncN(...))` 或 `DataFuncN(...)` 包装调用,并自动汇聚所需 imports 与控制器注入字段。
|
||||
- 核心标签:`@Router` 定义路径与方法;`@Bind` 定义方法参数的来源与键名。
|
||||
- 生成行为:输出 `router.<METHOD>(path, FuncN(...))` 或 `DataFuncN(...)` 包装调用,并自动汇聚所需 imports 与控制器注入字段。
|
||||
|
||||
### 快速开始
|
||||
|
||||
```
|
||||
atomctl gen route [path]
|
||||
```
|
||||
|
||||
- 生成文件:当前包目录下 `routes.gen.go`
|
||||
- 分组与排序:按控制器分组,导入、方法、路由项稳定排序,便于审阅 diff。
|
||||
|
||||
### 注释语法
|
||||
|
||||
- `@Router <path> [<method>]`
|
||||
- 例如:`@Router /users/:id [get]`
|
||||
- 示例:`@Router /users/:id [get]`
|
||||
|
||||
- `@Bind <paramName> <position> [key(<key>)] [model(<field>|<model>[:<field>])]`
|
||||
- `paramName` 必须与方法参数名一致(大小写敏感)
|
||||
- `position` 取值:`path`、`query`、`body`、`header`、`cookie`、`local`、`file`
|
||||
- `@Bind <paramName> <position> [key(<key>)] [model(<field>|<model>[:<field>])]`
|
||||
- `paramName` 与方法参数名一致(大小写敏感)
|
||||
- `position`:`path`、`query`、`body`、`header`、`cookie`、`local`、`file`
|
||||
- 可选:
|
||||
- `key()` 覆盖默认键名;
|
||||
- `model()` 支持两种写法:
|
||||
- 仅字段:`model(id)`(推荐;模型类型由参数类型推断)
|
||||
- 类型+字段:`model(database/models.User:id)` 或 `model(database/models.User)`(字段缺省为 `id`)
|
||||
- `model()` 详见“模型绑定”。
|
||||
|
||||
### 参数来源与绑定生成
|
||||
### 参数绑定规则(按 position)
|
||||
|
||||
根据 `position` 与参数类型(是否标量)生成绑定器调用:
|
||||
|
||||
- query
|
||||
- 标量类型:`QueryParam[T]("key")`
|
||||
- 非标量类型:`Query[T]("key")`
|
||||
- path
|
||||
- 标量类型:`PathParam[T]("key")`
|
||||
- 非标量类型:`Path[T]("key")`
|
||||
- 若声明了 `model()` 且 position 为 `path`:生成 `PathModel[T]("field", "key")`,表示根据路径参数值按 `field` 查询并绑定为 `T` 实例(`T` 来自方法参数类型)。
|
||||
- query:标量用 `QueryParam[T]("key")`,非标量用 `Query[T]("key")`
|
||||
- path:标量用 `PathParam[T]("key")`,非标量用 `Path[T]("key")`
|
||||
- 若使用 `model()`(仅在 path 有效),会按字段值查询并绑定为 `T`,详见下文
|
||||
- header:`Header[T]("key")`
|
||||
- body:`Body[T]("key")`
|
||||
- cookie
|
||||
- `string`:`CookieParam("key")`
|
||||
- 其他:`Cookie[T]("key")`
|
||||
- cookie:`string` 用 `CookieParam("key")`,其他用 `Cookie[T]("key")`
|
||||
- file:`File[multipart.FileHeader]("key")`
|
||||
- local:`Local[T]("key")`
|
||||
|
||||
说明:
|
||||
|
||||
- 标量类型集合(影响是否使用 `Param` 后缀):`string`、`int`、`int32`、`int64`、`float32`、`float64`、`bool`。
|
||||
- `key` 默认与 `paramName` 相同;若设置 `key(...)` 则使用其值。
|
||||
- `file` 使用固定类型参数 `multipart.FileHeader`(请确保依赖可用)。
|
||||
- 标量类型集合:`string`、`int`、`int32`、`int64`、`float32`、`float64`、`bool`
|
||||
- `key` 默认等于 `paramName`;设置 `key(...)` 后以其为准
|
||||
- `file` 使用固定类型 `multipart.FileHeader`
|
||||
|
||||
### 类型与指针处理
|
||||
|
||||
- 支持 `T`、`*T`、`pkg.T`、`*pkg.T` 形式;会正确记录选择子表达式对应的 import。
|
||||
- 忽略结尾为 `Context` 或 `Ctx` 的参数(视为框架上下文)。
|
||||
- 指针处理:除 `local` 外,会去掉前导 `*` 作为泛型实参;`local` 保留指针(便于写回)。
|
||||
- 支持 `T`、`*T`、`pkg.T`、`*pkg.T`;会正确收集选择子表达式对应 import
|
||||
- 忽略结尾为 `Context` 或 `Ctx` 的参数(框架上下文)
|
||||
- 指针处理:除 `local` 外会去掉前导 `*` 作为泛型实参;`local` 保留指针(便于写回)
|
||||
|
||||
### 解析与匹配规则
|
||||
### 解析与匹配
|
||||
|
||||
- 先收集注释中的多条 `@Bind`,再按“方法参数列表顺序”匹配并输出绑定器,确保调用顺序与方法签名一致。
|
||||
- 未在方法参数中的 `@Bind` 会被忽略;缺失 `@Router` 或方法无注释将跳过该方法。
|
||||
- import 自动收集去重;控制器注入字段名为类型名的小驼峰形式,例如:`userController *UserController`。
|
||||
- 先收集注释中的多条 `@Bind`,再按“方法参数列表顺序”匹配并输出绑定器,确保调用顺序与方法签名一致
|
||||
- 未在方法参数中的 `@Bind` 会被忽略;缺失 `@Router` 或方法无注释将跳过该方法
|
||||
- import 自动收集去重;控制器注入字段名为类型名的小驼峰形式,例如 `userController *UserController`
|
||||
|
||||
### 返回值与包装函数名
|
||||
### 返回值与包装函数
|
||||
|
||||
- 若方法返回值个数 > 1:使用 `DataFuncN`(包含数据与错误等返回)。
|
||||
- 否则使用 `FuncN`。
|
||||
- 其中 `N` 为参与绑定的参数个数。
|
||||
- 返回值个数 > 1:使用 `DataFuncN`
|
||||
- 否则使用 `FuncN`
|
||||
- `N` 为参与绑定的参数个数
|
||||
|
||||
### 示例
|
||||
### 模型绑定(path + model)
|
||||
|
||||
当 `@Bind ... model(...)` 配合 `position=path` 使用时,将根据路径参数值查询模型并绑定为方法参数类型的实例(`T` 来自方法参数)。
|
||||
|
||||
- 语法:
|
||||
- 仅字段:`model(id)`(推荐)
|
||||
- 指定字段与类型:`model(id:int)`、`model(code:string)`(用于非字符串路径参数)
|
||||
- 指定类型与字段:`model(pkg.Type:field)` 或 `model(pkg.Type)`(字段缺省为 `id`)
|
||||
- 行为:
|
||||
- 生成的绑定器会按给定字段构造查询条件并返回首条记录
|
||||
- 自动注入 import:`field "go.ipao.vip/gen/field"`,用于构造字段条件表达式
|
||||
|
||||
示例:
|
||||
|
||||
```go
|
||||
// @Router /users/:id [get]
|
||||
// @Bind user path key(id) model(id)
|
||||
func (uc *UserController) Show(ctx context.Context, user *models.User) (*UserDTO, error)
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
注释与方法签名:
|
||||
|
||||
@@ -90,6 +109,6 @@ router.Get("/users/:id", DataFunc4(
|
||||
|
||||
### 错误与限制
|
||||
|
||||
- 无效的 `@Router` 语法会报错;无效的 `position` 会在解析阶段触发错误。
|
||||
- `file` 目前仅支持单文件头;`model()` 仅在 `position=path` 时参与代码生成(使用 `PathModel`)。
|
||||
- 请与实际路由段保持一致(特别是 `path` 的 `key` 与路径变量名)。
|
||||
- 无效的 `@Router` 语法会报错;无效的 `position` 会在解析阶段触发错误
|
||||
- `file` 仅支持单文件头;`model()` 仅在 `position=path` 时参与代码生成
|
||||
- 请确保路由段变量名与 `key(...)` 保持一致
|
||||
|
||||
Reference in New Issue
Block a user