插件开发规范
使用命令创建插件
新插件应在 server_api 目录使用脚手架命令创建:
go run . plugin create example \
--name "示例插件" \
--author "开发者名称" \
--server-root . \
--admin-root ../admin_client \
--user-root ../user_client命令默认在核心同级创建 go_basic_frame_plugin_example 独立仓库,并生成后端、管理端、用户端子包、插件清单、数据库目录和三个版本文档。同时会在插件仓库 .dev 下复制精简核心代码,把插件源码同步到核心插件目录并生成静态注册文件,可直接进行宿主编译测试。
插件仓库根目录是发行源码的唯一来源;.dev 仅用于本地调试并由 Git 忽略。修改插件后执行:
./scripts/dev.sh增加版本时,先在宿主 server_api 目录执行 go run . plugin release <插件源码目录> --version <新版本> --incremental <增量工作文件>;无数据库变化使用 --no-database-change。命令自动生成全量、归档增量并同步版本声明,审核生成文档后再发行,详见数据库版本管理。
发行时执行:
./scripts/package.sh打包脚本只包含插件白名单目录,不会将 .dev 核心框架、node_modules 或 dist 打入 ZIP。只生成源码而不复制核心开发环境时可以使用 --skip-dev。
新开发者从核心版本建立插件仓库、制作发行包与安装升级的完整流程,参见插件安装与更新。
适用范围
本文档适用于基于当前项目开发的源码级业务插件,例如商城、新闻资讯、内容管理和营销活动等插件。
插件基线为核心 0.0.1(最低要求,推荐使用最新的版本开发),采用源码级、编译期注册模式:
- 不支持上传 Go 文件后动态加载,不支持运行中热启停;
- 插件安装、升级、启停后必须重新构建并重启管理端 API 和用户端 API;
- 插件后端和管理端页面都进入主项目构建流程。
强制原则
- 插件 ID 发布后不得修改:小写字母开头,仅含小写字母、数字和下划线,2~64 位;
- 插件间只能通过公开服务接口或明确声明的依赖协作,禁止直接修改其他插件的业务表;
- Controller 只负责通过
pkg/validate绑定参数、调用 Logic 和统一响应,业务规则及事务放在 Logic;插件请求字段必须同时声明binding规则、validate:"字段语义"和comment说明; - Logic 不直接返回 Model,接口响应必须在
resp中重新定义; - Model、Param、Resp 的字段必须添加 JSON、GORM 或校验 Tag,并写明字段注释;
- 表关联关系在 Resp 中定义,不在 Model 中定义;优先使用 GORM
Preload; - 管理端业务接口默认注册到
Permission路由组,禁止绕过登录、权限和操作日志; - 数据库表和字段必须有注释,字符集
utf8mb4,排序规则utf8mb4_general_ci; - 数据库枚举值必须从
1开始,并在 Go 枚举文件和前端枚举文件中集中定义,枚举优先查看核心框架是否有,没有的情况在自定义。 - 业务表中的文件字段只保存相对路径,展示时统一补全访问地址;
- 不使用 GORM
AutoMigrate,结构变化通过增量 SQL 交付; - 前端使用 TypeScript、ES6 与箭头函数,不得用
any绕过类型检查; - 插件页面复用现有组件、主题变量与交互规范,不得覆盖全局样式;
- 富文本编辑必须复用宿主
src/components/RichTextEditor.vue。 - 新业务表应保留内部自增
id,同时使用pkg/sn.Generate()创建带唯一索引的uid;公开接口和跨表业务关联统一使用uid,禁止把数据库id作为业务标识。核心历史表没有 UID 时,使用核心已经公开的业务编号(例如sys_member.sn)。
目录结构
后端
server_api/internal/plugins/{plugin_id}/
├── plugin.go # 清单、迁移声明、路由注册与生命周期
├── enums/ # 插件业务枚举(从 1 开始)
├── model/ # 插件自身的数据库表模型
├── service/ # 管理端、用户端共用的插件公开能力
├── admin/ # 管理端接口:controller / logic / param / resp
└── api/ # 用户端接口:controller / logic / param / respParam 与 Resp 按业务功能拆分文件(如 admin/param/article_create.go),禁止整个插件堆放在单一 param.go / resp.go。只有单端能力时可省略 admin 或 api 目录,但路由注册方法仍需实现并返回 nil。
管理端
admin_client/src/plugins/{plugin_id}/
├── api/ # 接口封装
├── types/ # 请求与响应类型
├── enums/ # 前端枚举
├── components/ # 表单等组件(与列表页拆分)
└── views/{view_path}/index.vue页面路径约定:数据库菜单路径 /plugin/{plugin_id}/{view_path} 对应 src/plugins/{plugin_id}/views/{view_path}/index.vue。
管理端动态图标统一通过核心 AppIcon 渲染。已有 Element Plus 图标名(如 Setting)继续兼容;新增 IconPark 图标使用 iconpark:Home 格式。IconPark 由核心本地依赖按需加载,禁止插件从 CDN 加载脚本、SVG 或把后端返回的 SVG 字符串直接插入 DOM。插件确需提供私有图标时,在 src/plugins/{plugin_id}/icons.ts 默认导出经过审查的本地 Vue/SVG 组件映射,菜单值使用 plugin:{plugin_id}:{icon_name},避免与核心或其他插件重名。
用户端
用户端采用 uni-app 独立分包和编译期注册:
user_client/src/plugins/{plugin_id}/
├── manifest.ts # 用户端插件清单
├── api/ # 接口请求
├── components/ # 插件私有组件
├── composables/ # 插件内组合式能力
├── constants/ # 插件私有常量与枚举
├── pages/ # 分包页面
├── types/ # 请求、响应与页面类型
├── utils/ # 插件私有无状态工具
└── static/ # 插件私有资源manifest.ts 必须声明插件 ID、名称、SemVer 版本、分包根目录、页面、入口页、最低核心版本和默认启用状态。插件 ID 与后端保持一致,subpackageRoot 使用 plugins/{plugin_id},入口路径使用 /plugins/{plugin_id}/pages/...。
用户端使用目录扫描式编译期注册:registry.ts 自动读取插件清单,Vite 自动扫描 src/plugins/* 为独立子包,不需要手工修改注册文件。
所有用户端插件必须放在 user_client/src/plugins/{plugin_id} 并作为 uni-app 独立子包。插件不得加入原生或自定义 TabBar,不得修改 TabBar 页面或在首页硬编码入口;入口统一由管理端配置的首页菜单跳转到插件子包。plugin install/upgrade --user-root ../user_client 会安装用户端源码,注册表与 Vite 在构建时自动扫描插件目录;安装后必须重新构建并完成多平台验收。
评论、点赞、收藏、分享等用户互动表必须保留内部自增 id,同时新增由 pkg/sn.Generate() 生成的唯一 uid;公开 API 和跨表业务关联只允许使用 uid。会员关系使用 sys_member.sn,并且只能从鉴权上下文取得,禁止客户端提交会员 SN。公开列表可放在 Public,写入、取消和个人状态查询必须放在 Auth。写接口需要幂等键、唯一索引、频率限制、事务计数和并发保护,用户端响应不得返回数据库主键、会员 SN、原始 IP 或其他敏感字段。
用户端开发还必须遵循:
- 请求统一使用
src/utils/request.ts,不得写死接口域名或另建 HTTP 客户端; - 一级、二级导航和安全区复用核心组件,交互反馈使用
appFeedback、AppFeedbackHost和AppConfirmDialog; - 禁止直接调用
uni.showToast、uni.showLoading、uni.showModal等原生提示 API; - 所有页面跳转统一使用
@/utils/navigation提供的appNavigator,禁止插件直接调用uni.navigateTo、uni.redirectTo、uni.reLaunch、uni.switchTab、uni.navigateBack或uni.navigateToMiniProgram; - 页面同时适配亮色、暗色和跟随系统,图标、文字、背景、边框、阴影使用共享主题变量;
- 业务布局使用 Flexbox,不使用 CSS Grid;首次加载使用结构匹配的骨架屏,刷新保留已有内容;
- 公共依赖安装在根工程,插件不维护独立
node_modules;新增依赖需评估多端兼容、重复依赖和分包体积; - H5、App 与声明支持的小程序平台均需完成构建和核心流程验证。
appNavigator 是核心框架提供的唯一页面导航入口,完整透传 uni-app 原生 options,因此原有的 success、fail、complete 和动画参数仍可使用。它统一覆盖普通页面、重定向、重启、TabBar、返回以及跳转其他小程序,并由核心层集中处理目标路由不存在或已经下线时的用户提示。插件不得自行重复实现路由不存在提示。
import { appNavigator } from '@/utils/navigation'
appNavigator.navigateTo({
url: `/plugins/example/pages/detail/index?id=${encodeURIComponent(id)}`,
})
appNavigator.navigateBack()迁移 SQL
database/{version}/
├── v0.0.1.sql
└── v1.1.0.sql插件契约
插件必须导出 New() 并实现 internal/common/plugin.Plugin:
package news
import (
"context"
commonplugin "server_api/internal/common/plugin"
)
type Plugin struct{}
func New() *Plugin { return &Plugin{} }
func (p *Plugin) Manifest() commonplugin.Manifest {
return commonplugin.Manifest{
PluginID: "news",
Name: "新闻资讯",
Version: "0.0.1",
CoreVersion: commonplugin.CoreVersion,
Dependencies: []commonplugin.Dependency{},
}
}
func (p *Plugin) Migrations() []commonplugin.Migration {
return []commonplugin.Migration{
{Version: "0.0.1", Description: "创建新闻资讯基础表", Checksum: "SQL 文件 SHA256"},
}
}
func (p *Plugin) RegisterAdminRoutes(ctx *commonplugin.Context, groups commonplugin.AdminRouteGroups) error {
groups.Permission.GET("/plugin/news/article/list", controller.List)
return nil
}
func (p *Plugin) RegisterAPIRoutes(ctx *commonplugin.Context, groups commonplugin.APIRouteGroups) error {
return nil
}
func (p *Plugin) Start(ctx context.Context, service commonplugin.ServiceType) error { return nil }
func (p *Plugin) Stop(ctx context.Context) error { return nil }清单要求:
plugin.json.version、后端Manifest.Version、Git 标签与 ZIP 文件名保持一致,遵循 SemVer;CoreVersion必须与实际宿主commonplugin.CoreVersion精确一致,不解析版本范围;- 所有依赖在
Dependencies中声明,精确匹配版本,禁止循环依赖; Version每次对外发布必须提升,禁止同版本重新打包。
安装器通过以下命令扫描插件并生成注册文件 internal/plugins/register_gen.go(禁止手工编辑):
go run . plugin generate --server-root .数据库启用但未编译进程序的插件会导致服务拒绝启动。
订单插件接入财务中心
产生订单的插件必须额外实现可选接口 commonplugin.FinanceOrderProvider,将订单投影到核心 sys_finance_order。核心财务列表只查询统一索引,不直接依赖任何插件订单表;完整订单和业务操作仍由来源插件维护。
func (p *Plugin) SyncFinanceOrders(ctx *commonplugin.Context) error {
latest, err := financeorder.LatestSourceUpdatedAt(ctx.App.DB, "example")
if err != nil { return err }
// 查询 updated_at >= latest 的来源订单并映射为 []financeorder.Record。
return financeorder.UpsertBatch(ctx.App.DB, records)
}- 使用
SourcePluginID + SourceOrderUID唯一关联订单,禁止使用数据库自增id; - 来源订单、订单项和跨表关联继续使用插件业务
uid; - 状态或金额变更必须更新来源订单
updated_at; - 支付状态统一为 1 未支付、2 已支付、3 已退款;
- 文件字段保存相对路径,由核心查询接口统一补全;
- 同步必须幂等并使用
financeorder.UpsertBatch处理并发冲突。
数据库规范
建表要求
CREATE TABLE IF NOT EXISTS `plg_news_article` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '文章ID',
`created_at` datetime(3) NOT NULL COMMENT '创建时间',
`updated_at` datetime(3) NOT NULL COMMENT '更新时间',
`deleted_at` datetime(3) DEFAULT NULL COMMENT '删除时间',
`title` varchar(200) COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章标题',
`status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '状态:1草稿,2已发布,3已下线',
PRIMARY KEY (`id`),
KEY `idx_news_article_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='新闻文章表';- 插件业务表统一
plg_{plugin_id}_*前缀,禁止使用sys_前缀; - 业务唯一性由唯一索引兜底;金额用整数最小货币单位或明确精度
decimal,禁止浮点数; - 迁移必须可安全重试(MySQL DDL 隐式提交,无法与元数据同事务);
- 已发布迁移不可修改,只能新增更高版本;每个版本必须提供全量和增量SQL;无变化时增量文件用注释说明,版本仍必须提升;
- 每个版本必须提供
docs/database/v{version}.md:业务表、表内关系、对核心表与其他插件表的引用、文件归属、Remove/Purge 影响;没有引用也要明确写「无」——缺少声明时安装器拒绝校验、安装、升级和 Purge。
迁移允许与禁止
允许:CREATE TABLE、ALTER TABLE、CREATE INDEX、对当前插件表的 INSERT(显式字段;增量不含自增 ID,自动生成的全量初始化数据可保留内部主键)。
拒绝:DROP、TRUNCATE、DELETE、RENAME、业务数据 UPDATE、破坏性 ALTER、存储过程/触发器、操作 sys_ 核心表或其他插件表、写入菜单与插件元数据。
路由与鉴权
| 路由组 | 使用场景 | 要求 |
|---|---|---|
Permission | 管理端普通业务接口 | 默认选择,登录 + 接口权限 + 操作日志完整链路 |
Auth | 登录即可访问的个人能力 | 必须说明不需要角色权限的原因 |
Public | 无登录态的回调或公开能力 | 必须自行实现验签、防重放、限流和幂等 |
用户端私有数据接口注册到 Auth,公开列表/详情或第三方回调注册到 Public;即使用户已登录,也必须在 Logic 中校验资源归属。
菜单与权限
- 菜单、按钮和接口权限在
plugin.json.menus中交付,不编写sys_menuINSERT SQL; - 菜单只用
key与parent_key描述层级,真实 ID 由数据库生成并记录到sys_plugin_menu; - 页面菜单路径必须位于
/plugin/{plugin_id}/下; - 普通角色默认不自动获得插件权限,由管理员明确授权。
API端鉴权
如果插件中涉及到用户登录状态的处理,在基础框架中已经实现了底层的封装,其核心思想是在登录鉴权的中间件,根据解析中的用户信息存放到了gin.Context的上下文中,因此你直接在业务逻辑层中根据上下文的方式就可以获取到当前登录用户的信息。 如果底层框架提供的查询字段不满足你的查询件需要,你可以自己根据用户ID(user_id)查询用户表,得到你想要的信息。
- 首先在插件中,你的路由要实现鉴权中间件。
// 需登录:仅接受会员令牌
_ := r.Group("/api/auth", commonmiddleware.Platform(), commonmiddleware.Auth(application, enums.ClientApi))
{
// 用户信息查询
auth.GET("/profile", authC.Profile)
}- controller需要将gin.Context传递到logic层,在logic层去获取到当前登录信息。
这里特别说一下,禁止在controller中去获取到用户信息,严格按照统一的格式。
// controller中的代码
func (h *AuthController) Profile(c *gin.Context) {
res, err := h.Logic.Profile(c)
if err != nil {
response.Fail(c, response.CodeErrBusiness, err.Error())
return
}
response.OK(c, res)
}// logic中获取当前登录信息
func (l *AuthLogic) Profile(c *gin.Context) (*resp.MemberProfile, error) {
// internal/common/auth/auth.go
claims := auth.CtxClaims(c)
userId := claims.UserID
member, err := l.currentMember(userId)
if err != nil {
return nil, err
}
item, err := l.toProfile(member)
if err != nil {
return nil, errors.New("头像地址解析失败")
}
return item, nil
}版本文档
每个插件版本必须提供(文件名版本与 plugin.json.version 完全一致):
docs/api/v{version}.md:覆盖管理端、用户端与回调接口,含参数、响应、鉴权、错误场景与版本变更记录;docs/database/v{version}.md:数据库引用说明(内容要求见上);docs/updates/v{version}.md:版本类型、主要变化、数据库变化、升级操作、兼容性与回滚方式。
自动生成的 OpenAPI/Swagger 可作补充,不能替代上述文档。
前端开发要点
- 请求统一走
request<T>(),接口域名只从环境变量读取; - 列表页与表单组件拆分,表单交互不写在列表页
index.vue; - 按钮使用
v-perm;时间展示到秒;文件字段只提交相对路径; - 明暗主题都要适配;不得定义跨模块的全局 CSS;
- 新增页面后执行
pnpm exec vue-tsc --noEmit与pnpm build确认可解析。
其中 v-perm 和 RichTextEditor 属于管理端规则;用户端使用独立分包、Wot Design Uni、Flexbox、共享主题变量与统一交互组件,不能直接套用管理端组件。用户端完整规范以仓库 user_client/docs/plugin-development.md 为准。
生命周期与后台任务
Start 只用于确实需要的后台任务、订阅或连接,普通 CRUD 插件返回 nil 即可。后台任务必须:
- 监听传入的
context.Context,取消后及时退出; - 避免重复启动,共享状态加锁或使用并发安全结构;
- 设置外部请求超时,可重试任务带退避与最大重试次数,消费任务设计幂等;
- 在
Stop中释放插件独占资源,不关闭宿主的数据库或 Redis 连接。
发布前检查清单
后端:插件 ID / 版本 / 依赖正确;分层合规;Logic 未返回 Model;关联无 N+1;管理端接口走 Permission;Public 接口有验签防重放限流;并发写入有唯一索引或锁;日志无敏感信息;gofmt / go vet / go build / go test 通过。
数据库:表前缀正确;注释齐全;utf8mb4_general_ci;枚举从 1 开始且前后端同步;索引合理;迁移可重试;版本与 SHA256 已登记;数据库引用文档完整。
管理端:页面路径符合解析约定;TS/ES6/箭头函数;表单拆分独立组件;v-perm 生效;明暗主题正常;时间到秒;无全局样式污染。
用户端:清单、注册表和分包扫描配置一致;未使用原生 TabBar、原生提示 API 和 uni.* 页面跳转 API,所有跳转均通过 appNavigator;Flexbox 布局;明暗与跟随系统主题正常;骨架屏、空状态和错误状态完整;H5 与声明支持的平台构建通过;主包和插件分包体积合规。
验证通过后仍需在测试环境完成安装、启用、权限分配、停用、升级与服务重启流程。
禁止事项
- 禁止绕过注册中心直接修改核心路由初始化;
- 禁止在插件启动时自动建表或修改表结构;
- 禁止把 Model 直接作为接口响应,禁止在 Model 中定义业务关联结构;
- 禁止跨插件直接改表、使用未校验的字符串拼接 SQL / 排序 / 表名;
- 禁止在 Public 路由中信任客户端身份字段;
- 禁止启动不能停止、没有超时或无限重试的 Goroutine;
- 禁止把密钥、Token、证书或用户敏感信息写入日志;
- 禁止覆盖全局 CSS、全局组件或核心菜单行为;
- 禁止修改已发布并登记摘要的迁移 SQL。
文档归档位置
宿主安装归档统一位于 go_basic_frame_wiki/plugin/archives/{plugin_id}/{version}。安装、升级和 Purge 使用相同的 --wiki-root,默认为后端同级 Wiki;数据库 SQL 仍保留在后端。详见文档索引与迁移说明。