插件计费接入

插件里如果涉及用户积分消耗或返还,优先使用:
ExtensionBillingModule
ExtensionBillingService
不要在插件里直接改user.power

1. 计费模块位置

packages/@chatbuddy-ai/extension-sdk/src/modules/billing/
├── extension-billing.module.ts
└── extension-billing.service.ts
底层仍然复用主程序的:
BaseBillingService
User
AccountLog

2. 为什么插件也必须走计费服务

因为插件计费服务除了加减积分,还会:
1.
校验余额
2.
account_log
3.
自动把来源记成插件来源
4.
从运行时堆栈识别当前插件标识符
5.
通过插件配置反查插件名称
如果你直接改user.power,会丢失:
流水来源
插件归属
运营/财务可追踪性

3. 如何注册ExtensionBillingModule

如果你的插件模块要用计费服务,建议在插件模块里显式引入:
import { ExtensionBillingModule } from "@chatbuddy-ai/extension-sdk"; @Module({ imports: [ExtensionBillingModule], providers: [ArticleService],
})
export class ArticleModule {}
虽然它被标记为@Global,但仍建议由插件某个根模块显式导入一次,避免运行时 provider 不可见。
对应的 service 注入写法:
@Injectable
export class ArticleService { constructor( private readonly extensionBillingService: ExtensionBillingService, ) {}
}

4.ExtensionBillingService提供的方法

hasSufficientPower(userId, requiredAmount)

判断用户余额是否足够:
const ok = await this.extensionBillingService.hasSufficientPower(userId, 5);
if (!ok) { throw HttpErrorFactory.badRequest("积分不足");
}

deductUserPower(options, entityManager?)

给插件业务扣费。

addUserPower(options, entityManager?)

给插件业务返积分或赠送积分。
一个最小可运行示例:
@Injectable
export class ArticleService { constructor( private readonly extensionBillingService: ExtensionBillingService, ) {} async polish(userId: string, taskNo: string) { await this.extensionBillingService.deductUserPower({ userId, amount: 5, remark: "文章润色消耗", associationNo: taskNo, }); }
}

5. 插件扣费参数怎么填

插件层传入的参数比主程序少一些,因为:
source
accountType
这两项由ExtensionBillingService自动补。
基础写法:
await this.extensionBillingService.deductUserPower({ userId, amount: 5, remark: "文章生成消耗", associationNo: taskNo,
});
底层会自动补成:
accountType: ACCOUNT_LOG_TYPE.PLUGIN_DEC
source.type: ACCOUNT_LOG_SOURCE.PLUGIN
source.source: 当前插件名称
所以插件只需关心:
用户是谁
扣多少
这次扣费属于哪个业务单号
备注怎么写

6. 插件加积分参数怎么填

await this.extensionBillingService.addUserPower({ userId, amount: 10, remark: "任务失败返还积分", associationNo: taskNo,
});
适合:
插件执行失败退回积分
运营活动赠送积分
业务补偿

7. 插件计费的一个关键注意点

当前实现里:
deductUserPoweraddUserPower
都会使用ACCOUNT_LOG_TYPE.PLUGIN_DEC
真正区分“增加还是扣减”的字段,是AccountLog.action
ACTION.DEC
ACTION.INC
所以如果你后续做插件账单报表,不要只按accountType判断增减,要结合:
accountType
action
一起看。

8. 插件名称是怎么识别出来的

ExtensionBillingService会通过调用栈识别当前插件标识符,再去扩展配置里取插件名称。
这意味着:
这个服务要从插件构建产物内部调用
不要把它脱离插件上下文拿到主程序其他位置去调用
否则可能出现:
找不到扩展标识符
抛出Extension not found

9. 插件计费的常见模式

模式一:执行前先校验最低积分

const ok = await this.extensionBillingService.hasSufficientPower(userId, 5);
if (!ok) { throw HttpErrorFactory.badRequest("积分不足,充值后再试");
}
适合:
长任务提交前
图片生成前
大模型工作流执行前

模式二:执行成功后按固定价格扣费

await this.extensionBillingService.deductUserPower({ userId, amount: 5, remark: "文章润色消耗", associationNo: taskNo,
});
适合:
固定单价功能
单次动作收费

模式三:执行失败返还积分

await this.extensionBillingService.addUserPower({ userId, amount: 5, remark: "文章润色失败返还", associationNo: taskNo,
});

模式四:按流程分段扣费

await this.extensionBillingService.deductUserPower({ userId, amount: 2, remark: "文档解析消耗", associationNo: taskNo,
}); await this.extensionBillingService.deductUserPower({ userId, amount: 3, remark: "摘要生成消耗", associationNo: taskNo,
});
适合:
多阶段工作流
每步成本不同的插件

10. 事务里的正确写法

如果插件里需“扣费 + 落业务数据”保证原子性,建议把entityManager传进去。
await this.articleRepository.manager.transaction(async (entityManager) => { await this.extensionBillingService.deductUserPower( { userId, amount: 5, remark: "文章生成消耗", associationNo: taskNo, }, entityManager, ); await entityManager.insert(ArticleTask, { userId, taskNo, status: "done", });
});
这样可以保证:
扣费成功但业务表写入失败时回滚
业务表写入成功但扣费失败时回滚

11. 插件计费最佳实践

预校验余额与实际扣费建议分两步
associationNo建议用任务号、订单号、工作流号
remark要能让运营一眼看懂
大额或关键扣费放事务里
不要绕过ExtensionBillingService自己改余额
做退款/返还时尽量和原业务单号保持一致,方便追踪

319 篇文档 · 内容同步自官方帮助中心