返回首页

瀑布流式感应

分类:shop项目
发布于:
阅读时间:45 分钟

“瀑布流式感应”(Service -> Controller -> Hooks)

1. 重新设计的“瀑布流”驱动模型

在这种模型下,Service 是逻辑的源头,下游自动感应。

1.扫描table.schema.ts 中的table ,带有@gen-skip 就跳过 不进行任何生成

 在 table.schema.ts 中:

1/**
2 * @gen-skip
3 */
4export const userRoleTable = ... // 中间表,跳过
5
6// 无注解 → 默认参与生成(但方法由 manifest 决定)
7export const tenantTable = ...
  • 第一级:Service (手动/模板驱动)

    • 脚本读取 manifest,确定该实体需要哪些方法。

    • 根据模板生成带 /** @generated */ 的 Service 方法。

    • 你也可以手写不带该标记的 Service 方法。

  • 第二级:Controller (Service 感应)

    • ControllerTask 扫描对应的 Service 类

    • 只要 Service 里有一个 public 方法(无论是自动生成的还是你手写的),Controller 就会尝试为其生成一个对应的 API 路由。

    • 通过 // @generated 标记来决定是否覆盖。

  • 第三级:Hooks (Controller 全量感应)

    • FrontendHookTask 扫描生成的 Controller 路由

    • 它不关心逻辑是怎么实现的,它只看 Controller 暴露了哪些 .get, .post 路径。

    • 即便你手写了一个复杂的路由,只要在 Controller 里,Hook 就能自动长出来。


2. 完美的增量更新:// @generated 哨兵模式

针对你最担心的 Elysia 链式调用和方法级同步,我们需要在代码生成逻辑中实现“原子化手术”。

Service 层(类方法级):

TypeScript

export class UserService {
  /** @generated */
  async list(ctx: ServiceContext) { ... } // 脚本会更新这里

  async customLogic(data: any) { ... }    // 脚本检测到没标记,直接跳过
}

Controller 层(链式调用级):

TypeScript

export const userController = new Elysia({ prefix: "/user" })
  // @generated
  .get("/", () => userService.list())     // 脚本检测到上方有标记,会根据 Service 的变化更新此处
  
  .get("/me", () => { ... })              // 脚本检测到没有标记,绝对不会碰这块代码
  
  // @generated
  .post("/", () => userService.create())

3. 实现这一架构的“三步走”方案

为了达到这个目标,我们需要对目前的脚本进行重构:

步骤 A:增强 AST 扫描器 (scanner.ts)

  • 引入 ts-morph 深度扫描 Service 类的所有方法。

  • 提取方法名、参数类型、返回类型,作为下一层的元数据。

步骤 B:实现 Elysia 路由注入器 (controller.task.ts)

  • 不再使用全量覆盖,而是解析链式调用。

  • 寻找带有 // @generated.get/post/put 调用节点。

  • 如果 Service 新增了方法,则在链式调用末尾追加新的路由块。

步骤 C:Hooks 的全量路径扫描 (frontend-hook.task.ts)

  • 遍历 user.controller.ts 的所有 CallExpression

  • 分析其 Method (get/post) 和 Path ("/", "/:id")。

  • 无论路由来源,统一生成对应的 React Query Hook。


4. 最终的开发体验

  1. 想增加功能? 只需要在 autogen.config.tsmanifest 里的 service 数组加一个 @export

  2. 运行 sync

    • userService 多了一个 export 方法。

    • userController 多了一个 .post("/export") 路由。

    • 前端多了 useExportUser Hook。

  3. 想手写特殊逻辑? 在 Controller 里直接写一个 .get("/complex"),不加 @generated。再次运行 sync 时,后端逻辑不变,但前端会自动生成 useComplexUser Hook。

这个流程的“爽点”在于自动化,而“难点”在于如何让生成的代码既符合规范又能灵活扩展。要让 Controller 自动感应 Service 并生成正确的路由,我们需要一套**约定优于配置(Convention over Configuration)**的机制来推断请求类型、路径和中间件。

以下是针对你提出的“请求类型、路径、参数、中间件”如何自动推导的设计方案:

1. 约定式请求类型与路径推导

我们不需要在 Service 里写复杂的装饰器,而是通过 Service 方法名的前缀 结合 参数契约 来实现自动推导。

Service 方法前缀推导请求 (Method)推导路径 (Path)参数来源说明
list /GET/query列表
get*GET/:idparams获取单条详情
create /POST/body新增记录
updatePUT/:idparams + body全量更新
patch*PATCH/:idparams + body局部更新状态
deleteDELETE/:idparams删除记录
treeGET/treequery树形列表
pageListGET/pagequery分页、过滤

自定义方法处理:

如果 Service 中出现不符合上述前缀的方法(如 syncData),则默认推导为 POST /sync-data,并接受 body 参数。


2. 参数类型的自动提取

利用 ts-morph 的 AST 解析能力,ControllerTask 在扫描 Service 时可以直接提取参数的类型信息。

  • 识别逻辑

    • 如果方法的参数中包含 id: string,Controller 自动注入 /:id 路径并校验 params

    • 如果参数中包含 query(对应 Contract 的 ListQuery),Controller 自动注入 query 校验。

    • 如果参数中包含 body(对应 Contract 的 CreateUpdate),Controller 自动注入 body 校验。

示例:

若 Service 定义为 update(id: string, body: UserContract["Update"]),生成的 Controller 会自动拼装为:

TypeScript

.put("/:id", ({ params, body }) => userService.update(params.id, body), {
  params: t.Object({ id: t.String() }),
  body: UserContract.Update
})

3. 中间件与第三个参数的模板化

Elysia 的第三个参数(包含权限、Hook、中间件等)可以通过**“元配置 + 局部重载”**的方式实现。

A. 默认配置模板 (Manifest 定义)

autogen.config.tsmanifest 中,为每个实体或每个层级定义默认的配置模板:

TypeScript

manifest: {
  user: {
    service: ["@list", "@create"],
    controller: {
      middleware: "authGuardMid", // 默认中间件模板
      requireDept: true           // 默认业务开关
    }
  }
}

B. 细粒度控制 (JSDoc 覆盖)

如果某个特定方法需要不同的中间件,可以在 Service 的方法上通过特定的 JSDoc 标签来覆盖默认行为。

TypeScript

export class UserService {
  /** * @route.middleware none 
   * @route.summary "公开获取列表"
   * @generated 
   */
  async listPublic(query: any) { ... }
}

生成器在扫描到 @route.* 标签时,会优先使用这些值来填充 Controller 的第三个参数。


4. 瀑布流感应流程:从 Service 到 Hook 的全自动链路

  1. Service 阶段:你定义了 async tree(ctx: ServiceContext) 方法。

  2. Controller 感应

    • ControllerTask 扫描到 tree 方法。

    • 由于方法名包含 tree 且无 id 参数,推导出 GET /tree

    • 生成带 // @generated 标记的代码块并插入 Controller。

  3. Hooks 全量感应

    • FrontendHookTask 扫描 Controller。

    • 识别到新的 .get("/tree") 路由。

    • 自动生成 useUserTree 钩子,包含正确的 QueryKey 和 API 调用。

5. 核心优势:为什么这很“爽”?

  • 零重复工作:你只需要关注 Service 里的业务逻辑,不需要再去 Controller 里手动复制一遍路径和权限。

  • 强类型保障:通过自动扫描参数类型,Controller 的校验对象与 Service 的参数类型永远是 1:1 同步的。

  • 手动/自动混排:通过 // @generated 哨兵模式,你可以在 Controller 里保留手写的 /me 等特殊接口,而常规的 CRUD 随 Service 自动起舞。


// autogen.config.ts 里的 manifest 部分
manifest: {
  "user": {
    // 逻辑层:决定 Service 生成哪些原子方法
    "service": ["@list", "@create", "@update", "@delete", "@customAction"],
    
    // 表现层:控制 Controller 的行为
    "controller": {
      "gen": true,             // 是否生成 Controller 文件
      "autoRoute": true,       // 是否根据 Service 方法自动映射路由
      "auth": "authGuardMid"   // 默认插件
    },

    // 客户端层:基于 Controller 结果的二次过滤
    "hooks": {
      "exclude": ["/internal-admin-only"], // 排除特定的路由不生成 Hook
    }
  }
}

2. “瀑布流式”生成逻辑:层层推导,而非底层决定

你提出的“前一层决定后一层”是极其高明的。我们不从 Table 直接推导出 Hook,而是建立如下依赖链:

  1. Service 扫描 Service 模板ServiceTask 扫描 templates/service/*.ts。如果你在 manifest 里给 User 配置了 @list,它就去找 list.ts 模板。

  2. Controller 扫描 Service 实例ControllerTask 不看 Table,它直接利用 ts-morph 扫描对应的 UserService 类。它发现 Service 有 findAll 方法,就自动生成对应的 .get("/") 路由。

  3. Hook 扫描 Controller 路由:这是最关键的一步。FrontendHookTask 扫描 user.controller.ts 的所有链式调用(.get, .post 等)。无论这个路由是自动生成的,还是你手写的,只要它存在于 Controller 中,Hook 就会自动长出来。

增强型 AST 更新:链式调用的“精准手术”

你提到的 Elysia 链式调用更新确实“逆天”,但通过 ts-morph 的节点寻址 是可以实现的。

策略:使用 // @generated 作为“防线”

在 Elysia 中,我们不再把注释写在整个变量上,而是写在调用链的方法前

代码生成器的视角:

TypeScript

export const userController = new Elysia({ prefix: "/user" })
  .use(dbPlugin)
  // @generated
  .get("/", () => userService.list()) // 这里的 .get 紧跟在注释后,它是安全的替换目标
  .get("/me", () => { ... })          // 这个方法没有注释,生成器会直接跳过它,即使它在链条中间
  // @generated
  .post("/", () => userService.create()) 

实现原理:

  1. 解析链条ts-morph 可以通过 getExpression() 一层层剥开 CallExpression

  2. 查找注释:利用 getLeadingCommentRanges() 检查当前调用节点(如 .get)上方是否有 // @generated

  3. 局部重组:如果检测到标记,通过 replaceWithText 仅替换该段链式代码。


3. 完整的自动化流程设计 (CLI 层)

第一步:autogen init (环境初始化)

  • 生成 autogen.config.ts

  • script/templates 下生成原子化的方法模板(例如 service/list.ts.txt)。

第二步:autogen scan (元数据同步)

  • 扫描 Schema:读取 table.schema.ts 里的 @gen 注释。

  • 智能识别

    • 如果有 @gen all,在 manifest 中为该实体开启所有层级。

    • 如果是中间表且没有注释,自动标记为 skip

  • 写入 Manifest:更新 autogen.config.ts 里的 manifest 对象。

第三步:autogen sync (多后端精准喷涂)

  • 多路径循环:读取 outputs 数组,依次访问每个项目目录。

  • 原子化注入

    • Service: 检查类方法上的 /** @generated */

    • Controller: 检查 Elysia 链条中的 // @generated

    • Frontend Hook: 检查导出函数上的 // @generated

  • 自动注册

    • 更新 index.ts

    • 更新 app-router.ts:寻找 appRouter 变量,插入 .use(xxxController)


4. 为什么这个结构对 AI 最友好?

  1. 明确的边界:AI 只要看到 // @generated,就知道这一块是它“可以接管”的领地;看到没有标记的代码,它会自动绕行。

  2. 模板的可预测性:由于模板被抽离到了 templates/ 文件夹,AI 可以非常容易地通过微调模板来改变全系统的生成风格,而不需要修改核心逻辑。

  3. 配置的紧凑性manifest 对象充当了 AI 的“任务清单”,它不需要扫描几千行代码,只需要看清单就知道哪些表需要生成哪些方法。

5. 建议的开发路线图

  1. 重构 ast-utils.ts:增加针对 CallExpression 链条的注释检查和替换逻辑。

  2. 实现 TemplateEngine:支持从外部文件夹加载方法体字符串,并替换 __TABLE__ 等占位符。