Repository navigation
Plugin 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。
分类为树形结构,可限制允许的履约类型与提供方。
定义在 @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。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /services |
当前用户的服务列表 |
| GET | /services/:id |
服务详情(合并提供方 getDetail) |
| GET | /services/:id/metrics |
监控时序(getMetrics) |
| POST | /services/:id/actions |
执行运维动作(executeAction) |
前端账户页:/account/services(列表)、/account/services/:id(详情)。由 store 插件的
frontend 包以 accountRoutes 声明。
-
manifest中requires: ['store'],onActivate注册fulfillment.provider扩展。 - 实现
provision:调用外部(或本地)逻辑开通服务,把凭证写入ctx.secrets并返回credentialsRef。 - 实现
getDetail/getMetrics/executeAction:读取外部数据并规范化返回。 - 商品创建时设置
fulfillmentType与providerId(可让管理员在 store 后台或同步插件里配置)。
示例:packages/plugins/zjmf-upstream 是首个完整实现(云服务器),
包含 provision(开通)、getDetail(ROOT/IP/状态)、getMetrics(CPU/内存)、
executeAction(开机/关机/重启)。
同步插件(如 zjmf-upstream)发现上游商品消失时,把本地商品置 DISCONTINUED(软停售,不硬删),
历史订单与交付物不受影响。管理员可通过 POST /zjmf-upstream/admin/mappings/remap
把本地商品重新关联到上游替换品并恢复在售。
核心开发
插件开发
主题开发
前端包开发
RESTful API