Packages 包@downcity/city

Hook 与服务挂载

hook 三层、service 挂载方式,以及它们为什么是 City 的共享扩展点。

Downcity 很多共享能力不是靠“把代码复制到每个 service 里”完成的,而是通过 hook 和 service 进入 City。

这两个概念分别在解决什么

可以先这样分工:

  • hook:给已有调用链插入规则
  • service:给 City 增加一整组长期复用能力

所以你看到的 usage、账号、支付,并不是“额外开一套系统”,而是作为扩展点进入同一套 Federation。

什么时候该先想 hook

  • 你想在某类调用前后加规则
  • 逻辑是横切的,不想写进每个 action
  • 能力只在当前项目里使用,不一定要独立成 package

典型例子:

  • 额度判断
  • 日志记录
  • 风控校验
  • 调用后扣减余额

什么时候更适合做 service

  • 能力需要跨多个产品复用
  • 不只是前后置逻辑,而是一整组 service / 表 / 管理入口
  • 你希望产品侧能长期通过统一 service 调它

典型例子:

  • @downcity/services
  • @downcity/services
  • @downcity/services

先理解 hook 的三层位置

global
  -> service
    -> action

含义分别是:

  • global:所有 service / action 共享
  • service:某个 service 下所有 action 共享
  • action:只对一个具体 action 生效

这三层不是为了增加复杂度,而是为了让你把规则放在最合适的位置,而不是一股脑写死到业务函数里。

最常见的 service 挂载示例

import { Federation } from "@downcity/city";
import {
  AccountsService,
  BalanceService,
  PaymentService,
  googleAccountsProvider,
  stripePaymentProvider,
  UsageService,
} from "@downcity/services";

const base = new Federation({ db });
const balance = new BalanceService();

base.use(new AccountsService({
  token_ttl: "7d",
  providers: [
    googleAccountsProvider(),
  ],
}));

base.use(balance);
base.use(new PaymentService({
  readTopup: async (topup_id) => await balance.readTopup(topup_id),
  finishTopup: async (topup_id, extra) => await balance.finishTopup(topup_id, extra),
  providers: [
    stripePaymentProvider({
      secret_key: process.env.STRIPE_SECRET_KEY,
      webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
    }),
  ],
}));
base.use(new UsageService({
  record_errors: true,
}));

这段代码背后的意思是:

  • new AccountsService() 给 City 增加 accounts service 容器
  • AccountsService 会自动挂载 /providers、统一登录入口、me、管理侧读取、better-auth passthrough 与 OAuth callback 入口;具体登录方式只有在对应 provider 挂载且配置满足后才会开放
  • new PaymentService() 给 City 增加统一支付入口,用来暴露当前可用的支付方式
  • new UsageService() 给 City 增加 usage 记录能力和相关查询入口
  • stripePaymentProvider() 给统一 Payment 服务增加 Stripe Checkout、webhook 和支付结果同步

service 挂进来后产品端怎么调

从产品视角看,service 最终会变成统一 service:

const login = await guest.service("accounts").action("login/start").invoke({
  provider: "email",
  city_id: "city_demo",
});

await guest.service("accounts").action("login/continue").invoke({
  login_id: login.login_id,
  input: {
    email: "[email protected]",
    password: "password123",
  },
});

const session = await guest.service("accounts").get("login/result", {
  login_id: login.login_id,
});

const usage = await admin.service("usage").get("summary");

const methods = await guest.service("payment").get("methods");

const checkout = await user.service("payment").action("checkout/create").invoke({
  method_id: "stripe",
  topup_id: "topup_demo",
});

也就是说,service 并不会让产品端学第二套调用协议。

hook、service 的关系

可以把它们想成一条链:

service / action
  -> hook 在调用前后加规则
  -> service 提供可复用 service、表和管理能力
  -> client 统一通过 /v1/* 调用

这条链的好处是:

  • 业务能力能复用
  • 规则能集中治理
  • 前端和后端的调用模型保持统一

常用 API / 入口

这一页最常用的公共入口其实很简单:

  • base.use(service):把 service 挂进 City
  • guest.service("accounts")...
  • guest.service("payment").get("methods")
  • user.service("usage")...
  • admin.service("payment").get("payments")

如果你是从扩展角度思考,最重要的不是记很多 API,而是先判断“这是 hook 问题还是 service 问题”。

常见误解

service 不是单纯的 npm 包

它代表的是一整组进入 City 的能力边界,通常还伴随:

  • service
  • 数据表
  • 管理接口
  • 可选 hook 逻辑

hook 不等于随便加中间件

hook 适合放横切治理逻辑,不适合把整个业务动作都塞进去。

service 不替代 City

service 负责把能力挂进 City,真正的产品调用入口仍然是 @downcity/city

继续阅读