后端开发
本文档详细介绍 ElasticView 插件后端开发的架构设计、API 开发、数据库操作和最佳实践。
技术栈
- Go 1.20+ - 编程语言
- eve-plugin-sdk-go - ElasticView 官方后端 SDK
- Gin - HTTP Web 框架(SDK内置)
- SQLite/MySQL - 数据库支持
项目结构
插件项目根目录/
├── backend/ # 后端代码目录
│ ├── router/ # 路由配置
│ │ └── router.go # 路由注册文件
│ ├── migrate/ # 数据库迁移(可选)
│ │ └── v0_0_1.go # 版本迁移文件
│ └── main.go # 程序入口
├── frontend/ # 前端代码目录
├── plugin.json # 插件配置文件(必需)
├── logo.png # 插件图标(必需)
├── go.mod
├── go.sum
└── README.md
快速开始
1. 程序入口(main.go)
go
package main
import (
"context"
"embed"
_ "embed"
"ev-plugin/backend/migrate"
"ev-plugin/backend/router"
"ev-plugin/frontend"
"flag"
"github.com/1340691923/eve-plugin-sdk-go/backend/plugin_server"
"github.com/1340691923/eve-plugin-sdk-go/build"
)
//go:embed plugin.json
var pluginJsonBytes []byte
//go:embed logo.png
var logoPng embed.FS
func main() {
// 必须先解析参数
flag.Parse()
// 启动插件服务
plugin_server.Serve(plugin_server.ServeOpts{
Assets: &plugin_server.Assets{
PluginJsonBytes: pluginJsonBytes, // plugin.json 插件配置,必填
FrontendFiles: frontend.StatisFs, // 前端工程编译后产物,必填
Icon: logoPng, // logo图标,必填
},
ReadyCallBack: func(ctx context.Context) {
// 插件就绪后的回调,可以在这里进行初始化操作,可以监听ctx管道从而实现优雅退出
},
Migration: &build.Gormigrate{Migrations: []*build.Migration{
migrate.V0_0_1(),
}}, // 插件存储的版本迁移配置,可选
RegisterRoutes: router.NewRouter, // 注册插件http接口路由,必填
})
}
2. 路由注册(router/router.go)
go
package router
import (
"github.com/1340691923/eve-plugin-sdk-go/backend/web_engine"
"github.com/1340691923/eve-plugin-sdk-go/ev_api"
"github.com/gin-gonic/gin"
"net/http"
)
// NewRouter 注册插件路由
func NewRouter(engine *web_engine.WebEngine) {
// 创建API路由组
userGroup := engine.Group("用户管理", "/api/user")
// 参数从左到右依次为:是否鉴权,接口备注,路由path,handler
userGroup.POST(true, "获取用户列表", "/list", getUserList)
userGroup.POST(true, "创建用户", "/create", createUser)
// 公开API(不需要鉴权)
publicGroup := engine.Group("公开接口", "/api/public")
publicGroup.POST(false, "健康检查", "/health", healthCheck)
}
// handle处理函数示例
func getUserList(c *gin.Context) {
// 获取EV API实例
api := ev_api.GetEvApi()
// 查询用户列表
var users []User
err := api.StoreSelect(c.Request.Context(), &users,
"SELECT * FROM users WHERE status = ? ORDER BY created_at DESC", 1)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
"code": 500,
})
return
}
//code 为 0 为成功,非0为异常
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": users,
"msg": "success",
})
}
func createUser(c *gin.Context) {
api := ev_api.GetEvApi()
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 400,
"msg": "参数错误: " + err.Error(),
})
return
}
// 插入用户数据
err := api.StoreSave(c.Request.Context(), "users", map[string]interface{}{
"name": req.Name,
"email": req.Email,
"age": req.Age,
})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "创建失败: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "创建成功",
})
}
func healthCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "OK",
"status": "healthy",
})
}
// 请求结构体
type CreateUserRequest struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"min=1,max=120"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
插件数据持久化操作(该api是用于插件数据存储,而非数据源操作)
基础操作
查询操作
go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"
// 获取全局EV API实例(内部已处理成单例)
api := ev_api.GetEvApi()
// 查询多条记录
var users []User
err := api.StoreSelect(ctx, &users, "SELECT * FROM users WHERE age > ?", 18)
// 查询单条记录
var user User
err := api.StoreFirst(ctx, &user, "SELECT * FROM users WHERE id = ?", userId)
执行操作
go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"
// 获取全局EV API实例(内部已处理成单例)
api := ev_api.GetEvApi()
// 执行SQL语句(INSERT、UPDATE、DELETE) 参数从左到右是 上下文,SQL,SQL执行参数
rowsAffected, err := api.StoreExec(ctx, "UPDATE users SET name = ? WHERE id = ?", "新名称", userId)
// 批量执行(内部有事务处理,要么全执行成功,要么全执行失败)
sqls := []dto.ExecSql{
{Sql: "INSERT INTO users (name) VALUES (?)", Args: []interface{}{"用户1"}},
{Sql: "INSERT INTO users (name) VALUES (?)", Args: []interface{}{"用户2"}},
}
err := api.StoreMoreExec(ctx, sqls)
便捷操作 类orm操作
go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"
// 获取全局EV API实例(内部已处理成单例)
api := ev_api.GetEvApi()
// 保存数据 参数从左到右是 上下文,表名,实体数据
err := api.StoreSave(ctx, "tableName", map[string]interface{}{
"name": "张三",
"email": "zhangsan@example.com",
"age": 25,
})
// 更新数据 参数从左到右是 上下文,表名,需修改实体数据,where条件SQL,SQL执行参数(对应 ? 传参)
rowsAffected, err := api.StoreUpdate(ctx, "tableName",
map[string]interface{}{"name": "李四"},
"id = ?", userId)
// 删除数据 参数从左到右是 上下文,表名,where条件SQL,SQL执行参数(对应 ? 传参)
rowsAffected, err := api.StoreDelete(ctx, "users", "id = ?", userId)
// 插入或更新(Upsert) 参数从左到右是 上下文,表名,需插入or更新实体数据,唯一键字段
err := api.StoreInsertOrUpdate(ctx, "tableName",
map[string]interface{}{
"setting_key": "theme",
"setting_value": "dark",
}, "setting_key")
插件版本升级时表结构需要变更的migrate操作
如果需要数据存储,可以创建版本迁移文件:
go
// migrate/v0_0_1.go
package migrate
import "github.com/1340691923/eve-plugin-sdk-go/build"
// V0_0_1 创建版本迁移,分别为 sqlite 和 mysql
func V0_0_1() *build.Migration {
return &build.Migration{
ID: "v0.0.1", // 版本号
SqliteMigrateSqls: []*build.ExecSql{ //当ev存储类型为sqlite时执行
{
Sql: `CREATE TABLE IF NOT EXISTS user_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
setting_key TEXT NOT NULL,
setting_value TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
Args: []interface{}{},
},
{
Sql: "CREATE INDEX idx_user_settings_user_id ON user_settings(user_id)",
Args: []interface{}{},
},
},
MysqlMigrateSqls: []*build.ExecSql{//当ev存储类型为mysql时执行
{
Sql: `CREATE TABLE IF NOT EXISTS user_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
setting_key VARCHAR(255) NOT NULL,
setting_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
Args: []interface{}{},
},
{
Sql: "CREATE INDEX idx_user_settings_user_id ON user_settings(user_id)",
Args: []interface{}{},
},
},
SqliteRollback: []*build.ExecSql{
{Sql: "DROP TABLE IF EXISTS user_settings", Args: []interface{}{}},
},
MysqlRollback: []*build.ExecSql{
{Sql: "DROP TABLE IF EXISTS user_settings", Args: []interface{}{}},
},
}
}
//V0_0_1用于 plugin_server.ServeOpts的Migration属性中
func main() {
// 启动插件服务
plugin_server.Serve(plugin_server.ServeOpts{
...
Migration: &build.Gormigrate{Migrations: []*build.Migration{
migrate.V0_0_1(),
}} // 插件存储的版本迁移配置,可选
})
}
SQL构建工具
SDK提供了便捷的SQL构建器,可实现类orm操作:
go
import "github.com/1340691923/eve-plugin-sdk-go/sql_builder"
// 使用SQL构建器
query := sql_builder.SqlBuilder.
Select("id", "name", "email").
From("users").
Where(sql_builder.And{
sql_builder.Eq{"status": 1},
sql_builder.Gt{"age": 18},
}).
OrderBy("created_at DESC").
Limit(10).
Offset(sql_builder.CreatePage(2, 10)) // 第2页,每页10条
sql, args, err := query.ToSql()
if err != nil {
// 处理错误
}
// 执行查询
var users []User
err = api.StoreSelect(ctx, &users, sql, args...)
第三方数据源操作
Elasticsearch操作
go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"
// dataSourceId 为数据源id,一般为插件前端传递的 sdk.GetSelectEsConnID()
api := ev_api.NewEvWrapApi(dataSourceId, util.GetEvUserID(ctx))
// 获取ES版本
version, err := api.EsVersion()
// ES搜索操作
searchRes, err := api.EsSearch(ctx, dto.SearchReq{
SearchReqData: dto.SearchReqData{
SearchRequest: proto.SearchRequest{
Index: []string{"my-index"},
Size: proto.IntPtr(100),
From: proto.IntPtr(0),
},
Query: map[string]interface{}{
"match_all": map[string]interface{}{},
},
},
})
// 复杂查询示例
complexSearchRes, err := api.EsSearch(ctx, dto.SearchReq{
SearchReqData: dto.SearchReqData{
SearchRequest: proto.SearchRequest{
Index: []string{"logs-*"},
Size: proto.IntPtr(50),
},
Query: map[string]interface{}{
"bool": map[string]interface{}{
"must": []interface{}{
map[string]interface{}{
"range": map[string]interface{}{
"@timestamp": map[string]interface{}{
"gte": "now-1h",
},
},
},
map[string]interface{}{
"term": map[string]interface{}{
"level": "ERROR",
},
},
},
},
},
},
})
SQL类型数据库数据源操作,该api操作可用于以下数据源(mysql,clickhouse,达梦,hive,MariaDB,oracle,Postgresql,sqlserver)
go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"
// dataSourceId 为数据源id,一般为插件前端传递的 sdk.GetSelectEsConnID()
api := ev_api.NewEvWrapApi(dataSourceId, util.GetEvUserID(ctx))
// MySQL查询多条记录 参数从左到右分别为 上下文,数据库名,执行sql语句,sql参数
columns, result, err := api.MysqlSelectSql(ctx,"my_database",
"SELECT * FROM products WHERE price >= ? and price <= ? ",100,110})
// MySQL查询单条记录 参数从左到右分别为 上下文,数据库名,执行sql语句,sql参数
result, err := api.MysqlFirstSql(ctx, "my_database",
"SELECT * FROM users WHERE id = ?",userId,
})
// MySQL执行增删改的SQL 参数从左到右分别为 上下文,数据库名,执行sql语句,sql参数
rowsAffected, err := api.MysqlExecSql(ctx, "my_database",
"UPDATE products SET price = 240 WHERE category = ?","electronics",
})
Redis操作
go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"
// dataSourceId 为数据源id,一般为插件前端传递的 sdk.GetSelectEsConnID()
api := ev_api.NewEvWrapApi(dataSourceId, util.GetEvUserID(ctx))
// Redis命令执行 参数从左到右分别为 上下文,数据库名,Redis命令参数
data, err := api.RedisExecCommand(ctx,0,"GET", "user:123"})
// Redis SET操作 参数从左到右分别为 上下文,数据库名,Redis命令参数
data, err := api.RedisExecCommand(ctx, 0,"SET", "user:123", "张三", "EX", 3600)
// Redis Hash操作 参数从左到右分别为 上下文,数据库名,Redis命令参数
data, err := api.RedisExecCommand(ctx, 0, "HGETALL", "user:profile:123")
/***
data 响应值类型为interface{} 以上代码相等于
import "github.com/go-redis/redis/v8"
data,err = redis.NewClient(options).Do(ctx, args...).Result()
*/
MongoDB操作
go
import "github.com/1340691923/eve-plugin-sdk-go/ev_api"
// dataSourceId 为数据源id,一般为插件前端传递的 sdk.GetSelectEsConnID()
api := ev_api.NewEvWrapApi(dataSourceId, util.GetEvUserID(ctx))
// 执行MongoDB查询命令 参数从左到右分别为 上下文,数据库名,MongoDB命令,超时时间
result, err := api.ExecMongoCommand(ctx,
"dbname", bson.D{
{"find", "users"},
{"filter", bson.D{
{"age", bson.D{{"$gt", 18}}},
{"status", "active"},
}},
{"limit", 100},
}, 30 * time.Second,
})
// MongoDB聚合查询 参数从左到右分别为 上下文,数据库名,MongoDB命令,超时时间
result, err := api.ExecMongoCommand(ctx,
"dbname", bson.D{
{"aggregate", "orders"},
{"pipeline", bson.A{
bson.D{{"$match", bson.D{{"status", "completed"}}}},
bson.D{{"$group", bson.D{
{"_id", "$category"},
{"total", bson.D{{"$sum", "$amount"}}},
{"count", bson.D{{"$sum", 1}}},
}}},
bson.D{{"$sort", bson.D{{"total", -1}}}},
}},
{"cursor", bson.D{}},
}, 60 * time.Second,
})
Q: 为什么我的代码没有问题,但是页面一片空白?
A: 可能是因为plugin.json中的frontend_debug选项没有设置为true。
注意事项
- 必须调用 flag.Parse() - 在主程序开始时必须先调用
- 资源嵌入 - 使用
//go:embed
嵌入必要的资源文件