基本介绍
LinaPro主框架通过capability.Services接口向插件暴露一组稳定的基础能力服务。这些服务覆盖了插件开发中最常见的横切关注点:认证与上下文、配置与资源、数据与存储、插件治理、通知、以及组织和租户等框架级能力。
源码插件通过pluginhost.Services获取完整的服务目录,它在capability.Services基础上扩展了TenantFilter(),为携带数据库查询构建器的能力提供了单独的入口。
这套服务架构遵循几个核心设计原则:
- 显式契约,稳定边界。 每个服务都有明确的合约定义(
contract包),插件只依赖稳定契约,不依赖宿主内部实现。 - 插件作用域隔离。 配置、缓存、清单资源等服务自动绑定到当前插件ID,插件间不会互相干扰。
- 能力可选,安全降级。 组织、租户等框架级能力在提供方不可用时自动降级,插件通过
Available()检查可用性。 - 只读消费,最小暴露。 普通插件获取的是只读消费接口,写操作和数据库查询构建器不通过
capability.Services暴露。
能力速查
| 分类 | 服务 | 合约类型 | 简介 |
|---|---|---|---|
| 认证与上下文 | APIDoc() | contract.APIDocService | API文档本地化,解析路由操作键和翻译文本 |
| 认证与上下文 | Auth() | contract.AuthService | 租户Token签发、切换和模拟令牌管理 |
| 认证与上下文 | BizCtx() | contract.BizCtxService | 读取当前请求的用户、租户、模拟状态等业务上下文快照 |
| 认证与上下文 | I18n() | contract.I18nService | 运行时翻译、获取请求Locale、搜索翻译键 |
| 配置与资源 | Config() | contract.ConfigService | 读取当前插件自己的静态配置 |
| 配置与资源 | HostConfig() | contract.HostConfigService | 读取宿主公开的配置白名单键 |
| 配置与资源 | Manifest() | contract.ManifestService | 读取当前插件manifest/下的原始资源文件 |
| 数据与存储 | Cache() | contract.CacheService | 插件作用域的运行时缓存 |
| 数据与存储 | Session() | contract.SessionService | 在线会话管理:分页查询和踢出会话 |
| 数据与存储 | Route() | contract.RouteService | 获取当前动态路由的元数据 |
| 插件治理 | PluginLifecycle() | contract.PluginLifecycleService | 插件生命周期编排:租户级禁用/删除的前置检查和通知 |
| 插件治理 | PluginState() | contract.PluginStateService | 查询插件启用状态 |
| 通知 | Notify() | contract.NotifyService | 发布通知到宿主收件箱 |
| 能力提供方 | Org() | orgcap.Service | 组织能力消费:用户部门、岗位等只读投影 |
| 能力提供方 | Tenant() | tenantcap.Service | 租户能力消费:当前租户、可见性校验、租户切换 |
| 数据与存储 | TenantFilter() | contract.TenantFilterService | 为插件自有表注入tenant_id过滤条件 |
获取方式
源码插件
源码插件在路由注册、钩子回调和定时任务注册时,通过registrar.Services()获取完整的服务目录:
func registerRoutes(ctx context.Context, registrar pluginhost.HTTPRegistrar) error {
services := registrar.Services()
// 通过 services 访问各能力服务
config := services.Config()
tenantFilter := services.TenantFilter()
i18n := services.I18n()
// ...
return nil
}
registrar.Services()返回的pluginhost.Services嵌入了capability.Services的全部16个服务,并额外提供TenantFilter()。在钩子回调和定时任务注册中,同样通过payload.Services()或registrar.Services()获取。
动态插件
动态插件通过pluginbridge的guest侧SDK访问主框架能力。所有服务调用都经过hostServices桥接,必须先在plugin.yaml中声明授权:
hostServices:
- service: config
methods: [get]
- service: hostConfig
methods: [get]
resources:
keys:
- workspace.basePath
- i18n.default
- service: manifest
methods: [get]
resources:
paths:
- profile.yaml
- service: cache
methods: [get, set, delete, incr, expire]
- service: i18n
methods: [translate, getLocale]
- service: notify
methods: [send]
guest侧SDK提供的便捷函数(Config.String、Config.Duration、I18n.Translate、Manifest.Scan等)在运行时自动转化为对应的hostServices调用,不需要在plugin.yaml中把这些便捷函数名声明为methods。例如声明methods: [get]即可使用Config.String、Config.Bool、Config.Int、Config.Duration、Config.Scan等所有读取函数。
在WASM入口函数或控制器中,通过pluginbridge的guest侧能力获取各服务:
// 通过 guest 侧 SDK 读取配置
endpoint, err := guestsdk.Config.String(ctx, "sync.endpoint", "")
// 通过 guest 侧 SDK 读取宿主配置
workspaceBase, err := guestsdk.HostConfig.String(ctx, "workspace.basePath", "/admin")
// 通过 guest 侧 SDK 读取 manifest 资源
var profile struct {
Category string `yaml:"category"`
}
err := guestsdk.Manifest.Scan(ctx, "profile.yaml", "", &profile)
// 通过 guest 侧 SDK 读取翻译
message := guestsdk.I18n.Translate(ctx, "plugin.record.created", "Record created")
// 通过 hostServices 操作缓存
err := guestsdk.Cache.Set(ctx, "stats", "visit-count", "100", 0)
// 从 BridgeRequestEnvelopeV1 请求包中提取上下文
userID := requestEnvelope.UserID
tenantID := requestEnvelope.TenantID
动态插件的BizCtx上下文信息从BridgeRequestEnvelopeV1请求包中提取,而不是通过services.BizCtx()获取。请求包中的UserID、TenantID等字段由主框架在进入WASM沙箱前注入,语义与源码插件的BizCtxService.Current()一致。
代码示例
以下示例展示源码插件和动态插件使用各基础能力的常见模式。
读取配置
源码插件
源码插件直接调用services.Config()和services.HostConfig():
func registerRoutes(ctx context.Context, registrar pluginhost.HTTPRegistrar) error {
services := registrar.Services()
syncEndpoint, err := services.Config().String(ctx, "sync.endpoint", "")
if err != nil {
return err
}
timeout, err := services.Config().Duration(ctx, "sync.timeout", 30*time.Second)
if err != nil {
return err
}
workspaceBase, err := services.HostConfig().String(ctx, "workspace.basePath", "/admin")
if err != nil {
return err
}
_ = syncEndpoint
_ = timeout
_ = workspaceBase
return nil
}
动态插件
动态插件通过guest侧SDK读取,需要在plugin.yaml中声明config和hostConfig授权:
syncEndpoint, err := guestsdk.Config.String(ctx, "sync.endpoint", "")
if err != nil {
return err
}
timeout, err := guestsdk.Config.Duration(ctx, "sync.timeout", 30*time.Second)
if err != nil {
return err
}
workspaceBase, err := guestsdk.HostConfig.String(ctx, "workspace.basePath", "/admin")
if err != nil {
return err
}
操作缓存
源码插件
源码插件通过services.Cache()操作缓存:
func (s *recordService) GetVisitCount(ctx context.Context, recordID int) (int, error) {
services := s.services
val, err := services.Cache().Get(ctx, "visit", fmt.Sprintf("record:%d", recordID))
if err != nil {
return 0, err
}
count, _ := strconv.Atoi(val)
return count, nil
}
func (s *recordService) IncrVisitCount(ctx context.Context, recordID int) error {
services := s.services
_, err := services.Cache().Incr(ctx, "visit", fmt.Sprintf("record:%d", recordID))
return err
}
func (s *recordService) CacheSummary(ctx context.Context, recordID int, summary string) error {
services := s.services
return services.Cache().Set(ctx, "summary", fmt.Sprintf("record:%d", recordID), summary, 10*time.Minute)
}
动态插件
动态插件通过hostServices的cache授权操作缓存,需要在plugin.yaml中声明service: cache:
func GetVisitCount(ctx context.Context, recordID int) (int, error) {
val, err := guestsdk.Cache.Get(ctx, "visit", fmt.Sprintf("record:%d", recordID))
if err != nil {
return 0, err
}
count, _ := strconv.Atoi(val)
return count, nil
}
func IncrVisitCount(ctx context.Context, recordID int) error {
_, err := guestsdk.Cache.Incr(ctx, "visit", fmt.Sprintf("record:%d", recordID))
return err
}
国际化翻译
源码插件
源码插件通过services.I18n()翻译文本:
func (s *recordService) CreateRecord(ctx context.Context, req *CreateRecordReq) error {
services := s.services
// 插件语言包 manifest/i18n/zh-CN/plugin.json 中定义 "plugin.record.created"
msg := services.I18n().Translate(ctx, "plugin.record.created", "记录创建成功")
// 发布通知时使用翻译后的文本
return services.Notify().SendNoticePublication(ctx, &contract.NoticePublication{
SourceType: "plugin",
Title: msg,
Content: fmt.Sprintf("记录ID: %d", req.ID),
})
}
动态插件
动态插件通过guest侧SDK翻译,需要在plugin.yaml中声明service: i18n:
func CreateRecord(ctx context.Context, req *CreateRecordReq) error {
msg := guestsdk.I18n.Translate(ctx, "plugin.record.created", "记录创建成功")
// 发布通知
return guestsdk.Notify.SendNoticePublication(ctx, ¬ify.Publication{
SourceType: "plugin",
Title: msg,
Content: fmt.Sprintf("记录ID: %d", req.ID),
})
}
发布通知
源码插件
源码插件通过services.Notify()发布通知:
func (s *recordService) NotifyRecordCreated(ctx context.Context, record *Record) error {
services := s.services
title := services.I18n().Translate(ctx, "plugin.record.created.title", "新记录")
content := services.I18n().Translate(
ctx,
"plugin.record.created.content",
fmt.Sprintf("记录 \"%s\" 已创建", record.Title),
)
return services.Notify().SendNoticePublication(ctx, &contract.NoticePublication{
SourceType: "plugin",
CategoryCode: "record_created",
Title: title,
Content: content,
})
}
动态插件
动态插件通过guest侧SDK发布通知,需要在plugin.yaml中声明service: notify:
func NotifyRecordCreated(ctx context.Context, record *Record) error {
title := guestsdk.I18n.Translate(ctx, "plugin.record.created.title", "新记录")
content := guestsdk.I18n.Translate(
ctx,
"plugin.record.created.content",
fmt.Sprintf("记录 \"%s\" 已创建", record.Title),
)
return guestsdk.Notify.SendNoticePublication(ctx, ¬ify.Publication{
SourceType: "plugin",
CategoryCode: "record_created",
Title: title,
Content: content,
})
}
租户感知
源码插件
源码插件通过TenantFilterService实现租户感知的数据操作。该服务在pluginhost.Services中扩展提供,不在capability.Services中。DAO层由make dao生成,提供dao.Xxx单例和do.Xxx、entity.Xxx模型类型。
服务层注入TenantFilterService:
type serviceImpl struct {
tenantFilter plugincontract.TenantFilterService
}
func New(tenantFilter plugincontract.TenantFilterService) Service {
return &serviceImpl{tenantFilter: tenantFilter}
}
在插件注册入口中,通过registrar.Services()获取并注入:
services := registrar.Services()
recordSvc := recordsvc.New(services.TenantFilter())
查询列表——使用Apply()追加tenant_id条件:
func (s *serviceImpl) List(ctx context.Context, pageNum, pageSize int) ([]*entitymodel.Record, int, error) {
model := s.tenantFilter.Apply(ctx, dao.Record.Ctx(ctx), "")
total, err := model.Count()
if err != nil {
return nil, 0, err
}
var items []*entitymodel.Record
err = model.OrderDesc(dao.Record.Columns().UpdatedAt).
Page(pageNum, pageSize).
Scan(&items)
if err != nil {
return nil, 0, err
}
return items, total, nil
}
联合查询时使用qualifier参数为tenant_id列添加表限定符,避免列名歧义:
func (s *serviceImpl) ListWithAuthor(ctx context.Context) ([]*RecordWithAuthor, error) {
model := dao.Record.Ctx(ctx).
LeftJoin("sys_user u", "u.id = "+dao.Record.Columns().AuthorId)
// qualifier 为 "r",生成 r.tenant_id 条件
model = s.tenantFilter.Apply(ctx, model, "r")
var items []*RecordWithAuthor
err := model.Scan(&items)
return items, err
}
新增记录——通过Context()获取租户ID并显式写入:
func (s *serviceImpl) Create(ctx context.Context, in *CreateRecordInput) (int64, error) {
tenantID := s.tenantFilter.Context(ctx).TenantID
recordID, err := dao.Record.Ctx(ctx).Data(do.Record{
TenantId: tenantID,
Title: strings.TrimSpace(in.Title),
Content: in.Content,
}).InsertAndGetId()
if err != nil {
return 0, err
}
return recordID, nil
}
更新和删除——手动添加TenantFilterColumn条件,与主键一起限定范围:
func (s *serviceImpl) Update(ctx context.Context, in *UpdateRecordInput) error {
tenantID := s.tenantFilter.Context(ctx).TenantID
_, err := dao.Record.Ctx(ctx).
Where(plugincontract.TenantFilterColumn, tenantID).
Where(do.Record{Id: in.Id}).
Data(do.Record{
Title: strings.TrimSpace(in.Title),
Content: in.Content,
}).
Update()
return err
}
func (s *serviceImpl) Delete(ctx context.Context, id int64) error {
tenantID := s.tenantFilter.Context(ctx).TenantID
_, err := dao.Record.Ctx(ctx).
Where(plugincontract.TenantFilterColumn, tenantID).
Where(do.Record{Id: id}).
Delete()
return err
}
动态插件
动态插件有些不一样,由于设计沙箱的权限控制,动态插件需要通过hostServices的data授权进行数据库查询,租户过滤由主框架在数据访问层自动注入:
# plugin.yaml
hostServices:
- service: data
methods: [list, get, create, update, delete]
resources:
tables:
- plugin_demo_dynamic_record
动态插件中的数据库操作示例:
type recordDAO struct{}
func (d *recordDAO) ListByTenant(ctx context.Context, page, pageSize int) ([]*Record, error) {
result, err := guestsdk.Data.List(ctx, &data.ListRequest{
Table: "plugin_demo_dynamic_record",
Page: page,
PageSize: pageSize,
})
if err != nil {
return nil, err
}
var records []*Record
if err := result.Scan(&records); err != nil {
return nil, err
}
return records, nil
}
相关内容
APIDocService
API 文档本地化服务的设计动机、操作键构建机制和在请求链路中的位置,帮助插件开发者理解路由操作键如何映射到本地化的模块标签和操作摘要。
AuthService
认证 Token 交接服务的两阶段认证模型、模拟令牌治理边界和在多租户认证流程中的位置,帮助插件开发者理解租户 Token 签发、切换和模拟的设计约束。
BizCtxService
业务上下文投影服务的设计约束、CurrentContext 字段含义和在请求链路中的位置,帮助插件开发者理解如何读取当前请求的用户、租户、模拟状态等上下文信息。
CacheService
插件作用域缓存的隔离机制、缓存值类型、过期策略和设计约束,帮助插件开发者理解缓存服务如何在多插件和多租户环境下保证数据隔离和安全降级。
ConfigService 与 HostConfigService
插件配置与宿主配置的读取优先级、白名单边界和设计约束,帮助插件开发者理解配置服务的隔离机制和正确使用方式。
I18nService
运行时翻译服务的设计思路、翻译键命名约定和与插件语言包的关系,帮助插件开发者理解如何在运行时根据请求 Locale 翻译插件文案。
ManifestService
插件清单资源服务的路径语义、资源管线边界和与配置服务的关系,帮助插件开发者理解如何读取 manifest/ 目录下的原始资源文件。
NotifyService
通知服务的发布模型、SourceType 和 CategoryCode 分类设计和在消息管线中的位置,帮助插件开发者理解如何通过通知服务将业务事件扇入宿主统一收件箱。
Org(组织能力)
消费侧消费侧只读接口设计、Provider 扩展机制、能力降级策略和在组织数据权限中的位置,帮助插件开发者理解如何消费和扩展组织能力。
PluginLifecycleService
插件生命周期编排服务在租户级治理中的角色、与 pluginhost.Lifecycle 的区别和设计约束,帮助插件开发者理解宿主如何编排跨插件的租户级生命周期事件。
PluginStateService
插件启用状态查询的本地快照与权威读取策略、Provider 启用状态的独立语义和设计约束,帮助插件开发者理解如何正确查询插件启用状态。
RouteService
动态路由元数据服务的设计定位、DynamicRouteMetadata 结构体和与动态插件的关系,帮助插件开发者理解如何获取当前动态路由请求的元数据信息。
SessionService
在线会话管理服务的设计定位、Session 投影模型和在会话治理中的位置,帮助插件开发者理解如何正确查询和管理在线用户会话。
Tenant(租户能力)
消费侧消费侧接口设计、Provider 和 Resolver 扩展机制、能力降级策略和在多租户架构中的位置,帮助插件开发者理解如何消费和扩展租户能力。
TenantFilterService
租户过滤服务的数据库查询注入模式、与 pluginhost.Services 的关系和在多租户数据隔离中的位置,帮助插件开发者理解如何为插件自有表注入租户过滤条件。