Skip to content

Plugin Fulfillment

worable edited this page Oct 12, 2026 · 1 revision

履约 / 交付物(Fulfillment)

StackPanel 的商品购买流程默认是「减库存 + 生成订单 + 支付」,支付后没有任何可交付物。 为了支持云服务器、发卡、会员、实物、API token 等全品类,商店插件(store)实现了通用的履约骨架, 每种商品类型由一个独立的「履约提供方插件」实现并注册。外部提供方(如智简魔方)只实现统一契约, 由平台统一读取并展示,数据在外部。

核心概念

  • 履约类型(FulfillmentType):交付方式,如 upstream_service(外部开通/云服务器)、card(发卡)、 instant(即时发放)、api_token(API 额度)、membership(会员)、physical(实物)、 donation(赞助)、topup(流量卡)。插件可声明自定义字符串类型。
  • 交付物(ServiceInstance):用户购买后获得的「那件事物」。「我的云服务」用户端的锚点。 凭证加密存 secrets,runtime 存可缓存的展示数据。
  • 履约提供方(FulfillmentProvider):把已付款商品变成交付物,并读取/展示服务数据。
  • 异步任务队列(DeliveryTask):付款后把订单项异步转化为交付物,支持退避重试。

数据模型

商店数据由 store 插件自治,不位于内核 Prisma schema。store 用 defineModel 声明六个模型(packages/plugins/store/src/data.ts),经 ctx.extensions / ctx.tx 读写:store/product、store/category、store/cart-item、store/order、store/service-instance、store/delivery-task。与履约相关的两个:

// store/service-instance
{
  userId, orderId: string | null, productId, productName,
  fulfillmentType, providerId: string | null, providerServiceId: string | null,
  state: string,            // 默认 PENDING_PROVISION;对外 API 暴露为 status
  credentialsRef: string | null, runtime: unknown, amount, currency,
  expiresAt: string | null, provisionedAt: string | null, terminatedAt: string | null,
}

// store/delivery-task
{
  orderId, productId, userId, productName, fulfillmentType,
  providerId: string | null, quantity: number,
  state: string,            // 默认 PENDING
  attempts: number, maxAttempts: number, error: string | null,
  payload: unknown, serviceId: string | null,
  nextAttemptAtMs: number,  // 下次可执行时刻(epoch 毫秒),0 表示无延迟
}

引擎约定:实体生命周期字段内部命名 state(可做原子条件更新),对外 API 仍以 status 暴露;时间字段用 epoch 毫秒(nextAttemptAtMs)而非 DateTime。

商品模型字段:categoryId、fulfillmentType、providerId、providerProductId。 分类为树形结构,可限制允许的履约类型与提供方。

履约提供方契约(SPI)

定义在 @stackpanel/sdk(packages/sdk/src/fulfillment.ts):

interface FulfillmentProvider {
  readonly id: string; // 如 'zjmf'
  readonly name: string;
  readonly fulfillmentTypes: string[]; // 本提供方支持的履约类型

  provision(ctx: ProvisionContext): Promise<ProvisionResult>;
  /** 可配置商品的权威计价;未实现则回退到商品静态价格。 */
  calculateCheckoutPrice?(ctx: CheckoutPriceContext): Promise<CheckoutPriceResult>;
  suspend?(ctx: ProviderServiceContext): Promise<void>;
  resume?(ctx: ProviderServiceContext): Promise<void>;
  terminate?(ctx: ProviderServiceContext): Promise<void>;
  getDetail?(ctx: ProviderServiceContext): Promise<ServiceDetail>;
  getMetrics?(ctx: ProviderServiceContext): Promise<MetricSeries[]>;
  executeAction?(actionId: string, ctx: ProviderServiceContext): Promise<void>;
}
  • provision:把已付款商品变成交付物。返回 { status, providerServiceId, credentialsRef, runtime }。 store 把自身 PluginSecrets 经 ProvisionContext.secrets 传入,提供方用它写入凭证并返回 credentialsRef 引用(约定 key 形如 fulfillment.<serviceId>);store 只保存引用,不保存明文。
  • getDetail / getMetrics / executeAction:只读 + 展示。用户「我的云服务」详情页会调用它们, 展示 ROOT 账户、IPv4、CPU/内存图表、开关机按钮等。
  • 注册方式:插件 onActivate 时 ctx.registerExtension('fulfillment.provider', provider)。

付款后履约流程

order.paid 事件
  → store 的 enqueueOrderFulfillment 为每一行创建 DeliveryTask(state=PENDING)
  → 内核任务 fulfillment-poll 消费到期任务(DB 乐观锁 claim 防并发)
  → 按 product.providerId 找到 FulfillmentProvider(无则回退到类型插件的本地履约)
  → provider.provision(...) → 创建 ServiceInstance + 写入凭证引用
  → 任务置终态;失败则按 nextAttemptAtMs 退避重试,耗尽 attempts 后置 FAILED
  • order.paid 由 store 自己的结算处理器在钱包支付 / 外部支付回调时发布。
  • 轮询通过 ctx.jobs.schedule('fulfillment-poll', { everyMs: 5000 }) 注册,由内核 worker 池执行,集群下只跑一次(不再每副本起定时器)。
  • 管理端可查看/重试任务:GET /store/admin/delivery-tasks、POST /store/admin/delivery-tasks/:id/retry。

用户「我的云服务」API

方法 路径 说明
GET /services 当前用户的服务列表
GET /services/:id 服务详情(合并提供方 getDetail)
GET /services/:id/metrics 监控时序(getMetrics)
POST /services/:id/actions 执行运维动作(executeAction)

前端账户页:/account/services(列表)、/account/services/:id(详情)。由 store 插件的 frontend 包以 accountRoutes 声明。

编写一个履约提供方插件

  1. manifest 中 requires: ['store'],onActivate 注册 fulfillment.provider 扩展。
  2. 实现 provision:调用外部(或本地)逻辑开通服务,把凭证写入 ctx.secrets 并返回 credentialsRef。
  3. 实现 getDetail / getMetrics / executeAction:读取外部数据并规范化返回。
  4. 商品创建时设置 fulfillmentType 与 providerId(可让管理员在 store 后台或同步插件里配置)。

示例:packages/plugins/zjmf-upstream 是首个完整实现(云服务器), 包含 provision(开通)、getDetail(ROOT/IP/状态)、getMetrics(CPU/内存)、 executeAction(开机/关机/重启)。

上游商品被删除的处理

同步插件(如 zjmf-upstream)发现上游商品消失时,把本地商品置 DISCONTINUED(软停售,不硬删), 历史订单与交付物不受影响。管理员可通过 POST /zjmf-upstream/admin/mappings/remap 把本地商品重新关联到上游替换品并恢复在售。

Clone this wiki locally