跳到主要内容
版本:0.2.x

基本介绍

源码插件是LinaPro推荐的默认扩展方式。它以Go源码形式与主框架一起编译部署,使用pluginhost注册路由、钩子、定时任务、生命周期回调和治理逻辑,适合长期维护、性能要求高、需要完整工程体验的业务模块。

官方源码插件位于apps/lina-plugins/。主仓库通过该目录挂载官方插件工作区;用户项目也在该目录下维护自己的业务插件。

适用场景

场景是否推荐源码插件原因
长期业务模块推荐可测试、可审查、性能最好
组织、内容、监控等后台能力推荐与主框架权限、菜单、调度和多租户协作紧密
运行时热加载不优先源码插件需要重新构建和部署主框架
商业二进制分发不优先源码插件通常暴露源码

标准目录结构

apps/lina-plugins/<plugin-id>/
├── plugin.yaml
├── plugin_embed.go
├── backend/
│ ├── api/ # API DTO与路由契约
│ ├── hack/
│ │ └── config.yaml # make dao等插件开发配置
│ ├── internal/
│ │ ├── controller/ # HTTP控制器
│ │ ├── service/ # 业务服务层
│ │ ├── dao/ # make dao生成
│ │ └── model/ # do/entity模型
│ └── plugin.go # 插件注册入口
├── frontend/
│ ├── pages/ # 插件页面
│ └── slots/ # 插槽页面,可选
├── manifest/
│ ├── config/
│ │ ├── config.yaml # 开发期默认配置
│ │ └── config.example.yaml # 配置模板,不作为运行时默认值
│ ├── sql/ # 安装与升级SQL
│ │ ├── mock-data/ # 演示数据,可选
│ │ └── uninstall/ # 卸载SQL
│ └── i18n/ # 插件语言包
└── README.md

backend/internal/service/是插件服务逻辑的固定位置,不要在插件根目录或backend/根目录另建service/包。

插件清单

plugin.yaml声明插件身份、运行形态、多租户边界、菜单和权限:

id: content-article
name: 文章管理
version: v0.1.0
type: source
scope_nature: tenant_aware
supports_multi_tenant: true
default_install_mode: tenant_scoped
description: 提供文章内容的增删改查管理功能
author: linapro
license: Apache-2.0
menus:
- key: plugin:content-article:list
name: 文章管理
path: content-article-list
component: system/plugin/dynamic-page
perms: content-article:article:view
icon: ant-design:file-text-outlined
type: M
sort: 1
- key: plugin:content-article:create
parent_key: plugin:content-article:list
name: 创建文章
perms: content-article:article:create
type: B

菜单key必须全局唯一,推荐使用plugin:<plugin-id>:<menu-key>格式。按钮权限通过type: B挂在菜单下,不直接出现在侧边栏中。

数据库与SQL

插件安装SQL位于manifest/sql/,卸载SQL位于manifest/sql/uninstall/。安装和升级脚本必须幂等,常用CREATE TABLE IF NOT EXISTSCREATE INDEX IF NOT EXISTS等写法。

CREATE TABLE IF NOT EXISTS content_article_record (
"id" BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
"tenant_id" INT NOT NULL DEFAULT 0,
"title" VARCHAR(255) NOT NULL DEFAULT '',
"content" TEXT NOT NULL DEFAULT '',
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_content_article_record_tenant
ON content_article_record ("tenant_id");

需要支持多租户的插件表应包含tenant_id列。未启用multi-tenant插件时,tenant_id = 0表示平台上下文。

API与服务层

插件API定义同样使用g.Meta声明路径、方法、权限和文档说明:

type ArticleListReq struct {
g.Meta `path:"/articles" method:"get" tags:"Article" summary:"List articles" permission:"content-article:article:view"`
Page int `json:"page" v:"min:1"`
PageSize int `json:"pageSize" v:"min:1,max:100"`
}

这里的path是控制器绑定到插件路由组后的相对路径。源码插件业务API应统一挂载到/x/{plugin-id}/...,而不是占用主框架/api/v1控制面。例如上面的接口在linapro-content-article插件中推荐暴露为:

/x/linapro-content-article/articles

服务层通过插件自有DAO访问数据库。需要租户隔离时,应使用主框架发布的TenantFilterService追加租户条件,而不是手写不一致的过滤规则。

注册入口

源码插件在backend/plugin.go中通过init()注册:

func init() {
plugin := pluginhost.NewSourcePlugin("linapro-content-article")

plugin.Assets().UseEmbeddedFiles(contentarticle.EmbeddedFiles)

plugin.HTTP().RegisterRoutes(
pluginhost.ExtensionPointHTTPRouteRegister,
pluginhost.CallbackExecutionModeBlocking,
registerRoutes,
)

plugin.Cron().RegisterCron(
pluginhost.ExtensionPointCronRegister,
pluginhost.CallbackExecutionModeBlocking,
registerCronJobs,
)

plugin.Lifecycle().RegisterBeforeUpgradeHandler(beforeUpgrade)
plugin.Lifecycle().RegisterAfterUpgradeHandler(afterUpgrade)

pluginhost.RegisterSourcePlugin(plugin)
}

主框架在插件完整模式下生成聚合入口,空白导入已配置插件,使这些init()注册逻辑进入主框架进程。

路由注册

HTTPRegistrar.Routes()返回插件路由注册器。注册器的APIPrefix()当前返回插件专属命名空间/x/{plugin-id},后续路径段由插件自行组织:

func registerRoutes(ctx context.Context, registrar pluginhost.HTTPRegistrar) error {
routes := registrar.Routes()
middlewares := routes.Middlewares()

routes.Group(routes.APIPrefix(), func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.NeverDoneCtx(),
middlewares.HandlerResponse(),
middlewares.CORS(),
middlewares.RequestBodyLimit(),
middlewares.Ctx(),
)

group.Group("/", func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.Auth(),
middlewares.Tenancy(),
middlewares.Permission(),
)
group.Bind(articleController)
})
})
return nil
}

源码插件仍可注册非保留公开路由,例如/portal/.../assets/.../,用于门户页面、自管静态文件或SPA fallback。这类路由不属于插件API,不会自动投影为工作台菜单、权限或OpenAPI接口。源码插件不得在/x下注册其他插件的路径;注册冲突或越界路径会在启动阶段被拒绝。

插件配置和清单资源

源码插件通过registrar.HostServices()获取插件作用域主框架服务,其中配置和声明资源是最新版本中比较重要的能力:

服务用途
Config()读取当前插件自己的配置,生产覆盖路径为plugins/<plugin-id>/config.yaml,开发期默认路径为manifest/config/config.yaml
HostConfig()读取宿主公开配置白名单键,例如workspace.basePathi18n.defaulti18n.enabled
Manifest()读取当前插件manifest/下的声明资源,例如metadata.yaml

manifest/config/config.example.yaml只是模板,不参与默认读取。插件不应通过g.Cfg()扫描宿主完整配置树,也不应把插件业务配置写进主框架config.yaml

func registerRoutes(ctx context.Context, registrar pluginhost.HTTPRegistrar) error {
services := registrar.HostServices()

timeout, err := services.Config().Duration(ctx, "sync.timeout", 5*time.Second)
if err != nil {
return err
}
_ = timeout

workspaceBase, err := services.HostConfig().String(ctx, "workspace.basePath", "/admin")
if err != nil {
return err
}
_ = workspaceBase

var metadata struct {
Category string `yaml:"category"`
}
if err := services.Manifest().Scan(ctx, "metadata.yaml", "", &metadata); err != nil {
return err
}
return nil
}

前端页面

源码插件的前端页面位于frontend/pages/,由主框架工作台动态页壳加载。插件菜单中的component通常使用:

component: system/plugin/dynamic-page

页面可以复用默认工作台的前端生态和设计规范。插件禁用后,主框架菜单接口不再返回该插件入口,工作台侧边栏会自动隐藏。

源码插件也可以声明public_assets,让主框架把插件内可公开的静态资源托管到/x-assets/{plugin-id}/{version}/...。只有plugin.yaml中显式声明的资源目录会被公开;租户文件、用户私有文件、安装脚本和配置文件不应放入public_assets

事件钩子与定时任务

源码插件可以订阅主框架事件,例如登录成功、插件启用、系统启动等。钩子可以同步阻断,也可以异步执行,取决于注册时选择的执行模式。

插件也可以注册自己的定时任务处理器,供管理工作台创建任务时选择:

plugin.Cron().RegisterCron(
pluginhost.ExtensionPointCronRegister,
pluginhost.CallbackExecutionModeBlocking,
func(registry pluginhost.CronRegistry) error {
registry.Register("content-article:cleanup", cleanupExpiredArticles)
return nil
},
)

运行时升级

源码插件文件更新后,主框架会比较数据库中的有效版本和当前发现版本。发现更高版本时,插件进入pending_upgrade运行时状态,主框架基础治理能力保持可用,插件业务入口进入受控状态。

管理员在插件管理页执行显式运行时升级。升级流程会重新读取有效清单和目标清单,执行依赖检查、BeforeUpgrade回调、插件自定义升级逻辑、升级SQL、治理资源同步、有效版本切换和缓存失效。失败后进入upgrade_failed,可以查看诊断信息并重试。

这种模型避免把文件覆盖误认为数据和治理资源已经完成升级。

最佳实践

  • 插件ID使用kebab-case,数据库表前缀使用对应的snake_case
  • 安装和升级SQL必须幂等,避免保留数据后重新安装失败。
  • 服务逻辑放在backend/internal/service/
  • 插件只使用pluginhostpluginservice等稳定契约,不直接依赖主框架的internal/包。
  • 多租户插件表预留tenant_id列,并使用主框架租户过滤服务。
  • 菜单和按钮权限一并声明,避免页面可见但操作权限缺失。
  • 卸载时区分治理记录、数据库数据和文件数据,避免误删。