Packages 包@downcity/city

Hook 与服务挂载

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

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

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

可以先这样分工:

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

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

什么时候该先想 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 { CityBase } from "@downcity/city";
import { accountsService } from "@downcity/services";
import { balanceService } from "@downcity/services";
import { paymentService } from "@downcity/services";
import { stripePaymentMethod } from "@downcity/services";
import { usageService } from "@downcity/services";
import { stripePaymentService } from "@downcity/services";

const base = new CityBase({ db });
const balance = balanceService();

base.use(accountsService({
  token_ttl: "7d",
}));

base.use(balance);
base.use(paymentService({
  methods: [
    stripePaymentMethod(),
  ],
}));
base.use(usageService({
  record_errors: true,
}));

base.use(stripePaymentService({
  balance: {
    readTopup: async (topup_id) => await balance.readTopup(topup_id),
    finishTopup: async (topup_id, extra) => await balance.finishTopup(topup_id, extra),
  },
  secret_key: process.env.STRIPE_SECRET_KEY,
  webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
}));

这段代码背后的意思是:

  • accountsService() 给 City 增加 accounts service
  • paymentService() 给 City 增加统一支付入口,用来暴露当前可用的支付方式
  • usageService() 给 City 增加 usage 记录能力和相关查询入口
  • stripePaymentService() 给 City 增加 Stripe Checkout、webhook 和支付结果同步

service 挂进来后产品端怎么调

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

const session = await guest.service("accounts").action("login").invoke({
  email: "user@example.com",
  password: "password123",
  town_id: "town_demo",
});

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

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

const checkout = await user.service("payment.stripe").action("checkout/create").invoke({
  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.stripe")...

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

常见误解

service 不是单纯的 npm 包

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

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

hook 不等于随便加中间件

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

service 不替代 City

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

继续阅读