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 增加accountsservice 容器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 挂进 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/city。
继续阅读
- 想看 city 端怎么统一调用 service,读 @downcity/city
- 想看账号能力,读 @downcity/services
- 想看 usage 记录,读 @downcity/services
- 想看 Stripe 一次性充值,读 @downcity/services