Packages 包@downcity/federation
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 { AIService, Federation } from "@downcity/federation";
import {
AccountsService,
CreditsService,
PaymentService,
googleAccountsProvider,
stripePaymentProvider,
UsageService,
} from "@downcity/services";
const base = new Federation({ database });
const credits = new CreditsService();
const ai = new AIService({ credits });
base.use(new AccountsService({
token_ttl: "7d",
providers: [
googleAccountsProvider(),
],
}));
base.use(credits);
base.use(ai);
base.use(new PaymentService({
resolve_topup: ({ topup_amount_minor }) => ({ credits: topup_amount_minor * 10_000 }),
on_paid: async (record) => {
await credits.topup({
card: { kind: "primary", user_id: record.user_id },
credits: record.credits,
source: "payment",
ref: record.payment_id,
idempotency_key: `payment:${record.payment_id}`,
});
},
providers: [
stripePaymentProvider({
secret_key: process.env.STRIPE_SECRET_KEY,
webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
}),
],
}));
base.use(new UsageService({
ai_usage_reader: ai,
credits_usage_reader: credits,
}));这段代码背后的意思是:
new AccountsService()给 City 增加accountsservice 容器AccountsService会自动挂载/providers、统一登录入口、me、管理侧读取、better-auth passthrough 与 OAuth callback 入口;具体登录方式只有在对应 provider 挂载且配置满足后才会开放new PaymentService()给 City 增加统一支付入口,用来暴露当前可用的支付方式new UsageService(...)聚合AIService技术用量与CreditsService已入账消费,并提供当前用户查询入口stripePaymentProvider()给统一 Payment 服务增加 Stripe Checkout、webhook 和支付结果同步
service 挂进来后产品端怎么调
从产品视角看,service 最终会变成统一 service:
const login = await guest.service("accounts").action("login/start").invoke({
provider: "email",
bureau_id: "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 user.service("usage").get("me", {
from: "2026-08-01",
to: "2026-08-31",
timezone: "Asia/Shanghai",
});
const recent_usage = await user.service("usage").get("me/recent", {
limit: 20,
});
const methods = await guest.service("payment").get("methods");
const checkout = await user.service("payment").action("checkout/create").invoke({
method_id: "stripe",
topup_amount_minor: 500,
idempotency_key: "order_123",
});也就是说,service 并不会让产品端学第二套调用协议。
hook、service 的关系
可以把它们想成一条链:
service / action
-> hook 在调用前后加规则
-> service 提供可复用 service、表和管理能力
-> client 统一通过 /v1/* 调用这条链的好处是:
- 业务能力能复用
- 规则能集中治理
- 前端和后端的调用模型保持统一
常用 API / 入口
这一页最常用的公共入口其实很简单:
base.use(service):把 service 挂进 Cityguest.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/federation。
继续阅读
- 想看 city 端怎么统一调用 service,读 @downcity/federation
- 想看账号能力,读 @downcity/services
- 想看 usage 记录,读 @downcity/services
- 想看 Stripe 一次性充值,读 @downcity/services