Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
898cb72422 | ||
|
|
ff3d65db22 | ||
|
|
6a5a6e5370 | ||
|
|
27b8c31a52 | ||
|
|
c3618adb02 | ||
|
|
8b122816a1 | ||
|
|
e14aee1498 | ||
|
|
9446571363 | ||
|
|
53169264da | ||
|
|
9ad5d25836 | ||
|
|
7741f7584d | ||
|
|
aed3a5ac22 | ||
|
|
7216c11f0c | ||
|
|
abd333d9b6 | ||
|
|
8a4206375f | ||
|
|
a911a305e1 | ||
|
|
6675dca359 | ||
|
|
6f5c443c81 | ||
|
|
4869409bf7 | ||
|
|
911852d14e | ||
|
|
fefaa09793 | ||
|
|
4e99e723c9 | ||
|
|
e434c33afb | ||
|
|
beeb0840d5 | ||
|
|
dae5b130bb | ||
|
|
58511825d3 | ||
|
|
90a3c22545 | ||
|
|
895528e82c | ||
|
|
964c25efe1 |
@@ -1,40 +1,48 @@
|
||||
# git.fsdpf.net/go/contracts(v1-legacy)
|
||||
# git.fsdpf.net/go/contracts(master)
|
||||
|
||||
老架构下跨服务共享的契约层:HTTP/gRPC/队列/任务调度/定时任务/MQTT/WebSocket 等各类服务的接口
|
||||
定义,以及资源系统(`resource.go`)、用户审批(`user_approval.go`)等业务相关契约,供
|
||||
`framework`(服务编排/启动)和各业务项目共同依赖,避免互相直接 import 造成循环依赖。
|
||||
跨服务共享的契约层:各类服务(HTTP/gRPC/队列/任务调度/定时任务/MQTT/WebSocket/条件流引擎等)
|
||||
的接口定义,以及资源系统相关的类型别名,供 `framework-v2`(服务编排/启动)和各业务项目共同
|
||||
依赖,避免互相直接 import 造成循环依赖。配合 "-v2" 系列项目使用。
|
||||
|
||||
## 这个分支有什么
|
||||
## 这个仓库有什么
|
||||
|
||||
- `resource.go`/`orm.go`:跟老版 `req`/`orm` 配套的资源、ORM 相关契约。
|
||||
- `http.go`/`grpc.go`/`grpcall.go`/`server.go`:HTTP/gRPC 服务契约。
|
||||
- `job.go`/`cron.go`/`queue.go`:任务调度、定时任务、消息队列契约。
|
||||
- `mqtt.go`/`ws.go`:MQTT、WebSocket 契约。
|
||||
- `jwt_auth.go`/`captcha.go`/`user_approval.go`:鉴权、验证码、用户审批相关契约。
|
||||
- `res_event.go`:资源变更事件契约(新架构里拆成了 `res_watcher.go`/`event_stream.go`,接口
|
||||
形状不一样)。
|
||||
- `res_type/`:资源类型相关的独立子包(新架构里已经不存在,能力并到别处了)。
|
||||
- `base/`/`support/`:基础类型和辅助工具。
|
||||
### DI 辅助(`di.go`)
|
||||
|
||||
## 这个分支的定位
|
||||
对 `samber/do/v2` 的一层薄封装:`Injector`/`Invoke`/`MustInvoke`/`InvokeNamed`/
|
||||
`MustInvokeNamed`,全仓库统一用这几个函数取 DI 容器里的服务,不直接 import `samber/do/v2`。
|
||||
|
||||
给还在维护、但还没迁移到新架构(`master` 分支上新增的 `di.go` DI 辅助函数、`app.go`、
|
||||
`condflow.go`、`gobridge.go`、`mcp.go`、`res_virtual_table.go`/`res_watcher.go` 等)的老项目用,
|
||||
比如 `zg-estate`(直接依赖)。只接受独立于新架构的 bug 修复。
|
||||
### 插件与注册(`app.go`)
|
||||
|
||||
## 依赖方怎么安装/更新
|
||||
`App` 接口:`LoadGoPlugin(file string) error` 加载 Go plugin,`RegControllers`/`RegResWatchers`/
|
||||
`RegGRpcs`/`RegJobs`/`RegCrons`/`RegMqtts`/`RegFlowDecisions`/`RegGoBridges`/
|
||||
`RegResVirtualTables` 一批 `Reg*` 方法把插件里定义的各类实现注册进框架,对应的 `GetApp*`/
|
||||
`Get*` 方法按 code 查回来。
|
||||
|
||||
Go modules 不会记住某个版本是从哪个分支解析出来的,`go.mod`/`go.sum` 里存的只是一次性解析出
|
||||
的 commit 伪版本号,分支上有新提交也不会自动同步。
|
||||
### 资源系统对接
|
||||
|
||||
**首次锁定,或者要拿这个分支上新提交的更新**,都执行同一条命令:
|
||||
- `resource.go`:`GetResource`/`MustResource` 是 `req.GetResource`/`req.MustResource` 的类型
|
||||
别名,`GetUser` 按 uuid 查用户。
|
||||
- `res_watcher.go`:`ResEventType`(`resx.ResEventType` 别名)+ `ResWatcher`——资源发生变更时
|
||||
要通知的外部监听方注册信息。
|
||||
- `res_virtual_table.go`:`ResVirtualTableSetup`(`req.ResVirtualTableSetup` 别名)——`framework-v2`
|
||||
在 `sqlite_vtable` build tag 下注册进容器,负责给 SQLite 注册虚拟表模块。
|
||||
- `event_stream.go`:事件流相关契约。
|
||||
|
||||
```
|
||||
go get git.fsdpf.net/go/contracts@v1-legacy
|
||||
```
|
||||
### 各类服务契约
|
||||
|
||||
**禁止**跑裸的 `go get -u`(或者 `go get git.fsdpf.net/go/contracts@latest`)——这个仓库没有打
|
||||
语义化 tag,Go 对"最新版本"的解析规则是退回到仓库默认分支(`master`)的 HEAD,会把新架构的
|
||||
破坏性改动一起拉进来,而不是停留在这个分支上。同时注意这条老线依赖的 `git.fsdpf.net/go/db`/
|
||||
`git.fsdpf.net/go/req`/`git.fsdpf.net/go/orm` 也要一起锁定在它们各自的 `v1-legacy` 分支,
|
||||
不能新老混用。
|
||||
`http.go`/`grpc.go`/`grpcall.go`/`server.go`(HTTP/gRPC)、`job.go`/`cron.go`/`queue.go`
|
||||
(任务调度/定时任务/消息队列)、`mqtt.go`/`ws.go`(MQTT/WebSocket)、`jwt_auth.go`/`captcha.go`
|
||||
(鉴权/验证码)、`condflow.go`(条件流引擎对接)、`gobridge.go`(跨语言 bridge)、`mcp.go`
|
||||
(Model Context Protocol 对接)、`executor.go`(通用执行器契约)。
|
||||
|
||||
### 子包
|
||||
|
||||
- `base/`:基础类型(用户、角色等业务通用结构)。
|
||||
- `support/`:辅助工具。
|
||||
|
||||
## 分支状态
|
||||
|
||||
`master` 是持续开发中的新架构,**跟老仓库 `v1-legacy` 分支不完全兼容**——`di.go`/`app.go`/
|
||||
`condflow.go`/`gobridge.go`/`mcp.go`/`res_virtual_table.go`/`res_watcher.go` 都是新增的,
|
||||
`res_event.go`/`res_type/` 这些老概念已经拆分/移除。还在用老版契约的老项目,参见 `v1-legacy`
|
||||
分支。
|
||||
|
||||
+10
-3
@@ -1,17 +1,24 @@
|
||||
package contracts
|
||||
|
||||
type Service interface {
|
||||
type App interface {
|
||||
LoadGoPlugin(file string) error
|
||||
RegControllers(items map[string]Controller) error
|
||||
RegListens(items map[string]ResListener) error
|
||||
RegResWatchers(items map[string]ResWatcher) error
|
||||
RegGRpcs(items map[string]GRpc) error
|
||||
RegJobs(items map[string]Job) error
|
||||
RegCrons(items map[string]Cron) error
|
||||
RegMqtts(items map[string]Mqtt) error
|
||||
GetResListener(code string) (ResListener, bool)
|
||||
RegFlowDecisions(items map[string]FlowDecision) error
|
||||
RegGoBridges(items map[string]GoBridge) error
|
||||
RegResVirtualTables(items map[string]ResVirtualTable) error
|
||||
|
||||
GetResWatcher(code string) (ResWatcher, bool)
|
||||
GetAppController(code string) (Controller, bool)
|
||||
GetAppGRpc(code string) (GRpc, bool)
|
||||
GetAppJob(code string) (Job, bool)
|
||||
GetAppCron(code string) (Cron, bool)
|
||||
GetAppMqtt(code string) (Mqtt, bool)
|
||||
GetAppFlowDecision(code string) (FlowDecision, bool)
|
||||
GetAppGoBridge(code string) (GoBridge, bool)
|
||||
GetAppResVirtualTable(code string) (ResVirtualTable, bool)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package base
|
||||
|
||||
// DataType 定义参数数据类型
|
||||
type DataType string
|
||||
|
||||
const (
|
||||
DataTypeString DataType = "string" // 字符串类型
|
||||
DataTypeInteger DataType = "integer" // 整数类型
|
||||
DataTypeFloat DataType = "float" // 浮点数类型
|
||||
DataTypeBoolean DataType = "boolean" // 布尔类型
|
||||
DataTypeJSON DataType = "json" // JSON类型
|
||||
)
|
||||
|
||||
// SchemaParam 工作流参数
|
||||
type SchemaParam struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
Code string `db:"code" json:"code"`
|
||||
DataType DataType `db:"data_type" json:"data_type"`
|
||||
Required bool `db:"required" json:"required"`
|
||||
Comment string `db:"comment" json:"comment"`
|
||||
Fields SchemaParams `db:"fields" json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
type SchemaParams []SchemaParam
|
||||
|
||||
// Condflow 工作流
|
||||
type Condflow struct {
|
||||
Uuid string `db:"uuid" json:"uuid"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Code string `db:"code" json:"code"`
|
||||
Comment string `db:"comment" json:"comment"`
|
||||
Params SchemaParams `db:"params" json:"params"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
UpdatedAt string `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type GetCondflow func(code string) (Condflow, bool)
|
||||
@@ -0,0 +1,23 @@
|
||||
package base
|
||||
|
||||
import "git.fsdpf.net/go/reflux/fieldx"
|
||||
|
||||
// CondflowCase 工作流条件分支
|
||||
type CondflowCase struct {
|
||||
Uuid string `db:"uuid" json:"uuid"`
|
||||
CondflowUuid string `db:"condflow_uuid" json:"condflow_uuid"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Code string `db:"code" json:"code"`
|
||||
Priority int `db:"priority" json:"priority"`
|
||||
Action string `db:"action" json:"action"`
|
||||
ActionParams fieldx.Schema `db:"action_params" json:"action_params"` // 动作参数值, 对应 params_schema
|
||||
ActionConfig fieldx.Schema `db:"action_config" json:"action_config"` // 动作配置值, 对应 config_schema
|
||||
NextCaseOnSuccess bool `db:"next_case_on_success" json:"next_case_on_success"`
|
||||
NextFlowUuid string `db:"next_flow_uuid" json:"next_flow_uuid"`
|
||||
NextFlowParams fieldx.Schema `db:"next_flow_params" json:"next_flow_params"` // 流程间参数映射
|
||||
Comment string `db:"comment" json:"comment"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
UpdatedAt string `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type GetCondflowCases func(uuid string) []CondflowCase
|
||||
@@ -0,0 +1,25 @@
|
||||
package base
|
||||
|
||||
// CondflowDecisionAction 执行器动作定义
|
||||
type CondflowDecisionAction struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
Code string `db:"code" json:"code"`
|
||||
ConfigSchema SchemaParams `db:"config_schema" json:"config_schema"` // 配置项Schema定义
|
||||
ParamsSchema SchemaParams `db:"params_schema" json:"params_schema"` // 参数Schema定义
|
||||
ResourceUuid string `db:"resource_uuid" json:"resource_uuid"`
|
||||
Comment string `db:"comment" json:"comment"`
|
||||
}
|
||||
|
||||
// CondflowDecision 执行器注册表
|
||||
type CondflowDecision struct {
|
||||
Uuid string `db:"uuid" json:"uuid"`
|
||||
ResourceUuid string `db:"resource_uuid" json:"resource_uuid"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Code string `db:"code" json:"code"`
|
||||
Comment string `db:"comment" json:"comment"`
|
||||
Actions []CondflowDecisionAction `db:"actions" json:"actions"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
UpdatedAt string `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type GetCondflowDecision func(code string) (CondflowDecision, bool)
|
||||
@@ -51,6 +51,7 @@ type DataList struct {
|
||||
ThemeConfig map[string]any `db:"themeConfig"` // 列表布局配置
|
||||
Roles []string `db:"roles"` // 列表权限
|
||||
IsAuthDB int `db:"is_auth_db"` // 启用权限过滤
|
||||
SelectionType string `db:"selectionType"` // 行选择类型
|
||||
Platform string `db:"platform"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
|
||||
@@ -25,6 +25,7 @@ type DataListField struct {
|
||||
FilterByJsSetting map[string]any `db:"filterByJsSetting"` // 字段筛选组件设置参数
|
||||
FilterOperator string `db:"filterOperator"` // 字段筛选符号
|
||||
EditableRoles []string `db:"editableRoles"` // 字段编辑权限
|
||||
Middlewares [][2]any `db:"middlewares"` // 字段数据转换
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
@@ -5,25 +5,15 @@ type DataListOperation struct {
|
||||
Puuid string `db:"pUuid"`
|
||||
DataListUuid string `db:"data_list_uuid"` // 所属列表 UUID
|
||||
Name string `db:"name"` // 按钮名称
|
||||
Code string `db:"code"` // 按钮标识
|
||||
Category string `db:"category"` // 按钮类型
|
||||
Icon string `db:"icon"` // 按钮图标
|
||||
Size string `db:"size"` // 按钮大小
|
||||
Block bool `db:"block"` // 固定宽度
|
||||
Ghost bool `db:"ghost"` // 背景透明
|
||||
Shape string `db:"shape"` // 按钮形状
|
||||
Type string `db:"type"` // 按钮样式
|
||||
IsPopConfirm bool `db:"isPopConfirm"` // 按钮提示
|
||||
PopTitle string `db:"popTitle"` // 按钮提示
|
||||
PopOkText string `db:"popOkText"` // 按钮提示
|
||||
PopCancelText string `db:"popCancelText"` // 按钮提示
|
||||
PopOkType string `db:"popOkType"` // 按钮提示
|
||||
PopPlacement string `db:"popPlacement"` // 按钮提示
|
||||
Widget string `db:"widget"` // 按钮组件
|
||||
WidgetType string `db:"widgetType"` // 组件类型
|
||||
WidgetProps map[string]any `db:"widgetProps"` // 组件PROPS
|
||||
WidgetSetting map[string]any `db:"widgetSetting"` // 组件设置
|
||||
WidgetContainerSetting map[string]any `db:"widgetContainerSetting"` // 组件容器设置
|
||||
Confirm map[string]any `db:"confirm"` // 按钮提示
|
||||
IsRefresh int `db:"isRefresh"` // 0不刷新, 1刷新所有数据, 2刷新一条数据
|
||||
NoAuthType string `db:"noAuthType"` // 无按钮权限处理方式
|
||||
Roles []string `db:"roles"` // 按钮权限
|
||||
|
||||
+19
-16
@@ -1,22 +1,25 @@
|
||||
package base
|
||||
|
||||
type GridLayout struct {
|
||||
Uuid string `db:"uuid"`
|
||||
ResourceUuid string `db:"resource_uuid"` // 所属资源UUID
|
||||
Code string `db:"code"` // 布局标识
|
||||
Name string `db:"name"` // 布局名称
|
||||
Type int `db:"type"` // 布局类型 [0=普通布局, 1=辅助布局]
|
||||
PrimaryKey string `db:"primaryKey"` // 主键
|
||||
MarginX int `db:"marginX"` // 栅格 x 间距
|
||||
MarginY int `db:"marginY"` // 栅格 y 间距
|
||||
Cols int `db:"cols"` // 网格列基数
|
||||
RowHeight float32 `db:"rowHeight"` // 栅格行高
|
||||
Css map[string]any `db:"css"` // 布局CSS
|
||||
Props []any `db:"props"` // 布局PROPS
|
||||
Platform string `db:"platform"`
|
||||
Roles []string `db:"roles"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
Uuid string `db:"uuid"`
|
||||
ResourceUuid string `db:"resource_uuid"` // 所属资源UUID
|
||||
Code string `db:"code"` // 布局标识
|
||||
Name string `db:"name"` // 布局名称
|
||||
Type int `db:"type"` // 布局类型 [0=普通布局, 1=辅助布局]
|
||||
PrimaryKey string `db:"primaryKey"` // 主键
|
||||
ItemMargin []any `db:"itemMargin"` // 栅格间距
|
||||
ContainerPadding []any `db:"containerPadding"` // 容器内边距
|
||||
Cols int `db:"cols"` // 网格列基数
|
||||
RowHeight float32 `db:"rowHeight"` // 栅格行高
|
||||
Css map[string]any `db:"css"` // 布局CSS
|
||||
Props []any `db:"props"` // 布局PROPS
|
||||
Groups []map[string]any `db:"groups"` // 布局分组
|
||||
Theme string `db:"theme"` // 布局样式
|
||||
ThemeConfig map[string]any `db:"themeConfig"` // 布局样式配置
|
||||
Platform string `db:"platform"`
|
||||
Roles []string `db:"roles"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
type GetGridLayout func(code string) (GridLayout, bool)
|
||||
|
||||
@@ -3,6 +3,7 @@ package base
|
||||
type GridLayoutField struct {
|
||||
Uuid string `db:"uuid"`
|
||||
GridLayoutUuid string `db:"grid_layout_uuid"` // 所属布局 UUID
|
||||
Group string `db:"group"`
|
||||
Type string `db:"type"`
|
||||
X int `db:"x"`
|
||||
Y int `db:"y"`
|
||||
|
||||
@@ -14,6 +14,9 @@ type GridLayoutForm struct {
|
||||
MarginY int `db:"marginY"` // 栅格 y 间距
|
||||
Cols int `db:"cols"` // 网格列基数
|
||||
FormProps []map[string]string `db:"formProps"` // 表单PROPS
|
||||
Groups []map[string]any `db:"groups"` // 布局分组
|
||||
Theme string `db:"theme"` // 布局样式
|
||||
ThemeConfig map[string]any `db:"themeConfig"` // 布局样式配置
|
||||
ListenChangeFields []string `db:"listenChangeFields"` // 监听字段
|
||||
ListenChangeFieldsFunc string `db:"listenChangeFieldsFunc"` // 监听字段回调方法
|
||||
Platform string `db:"platform"`
|
||||
|
||||
@@ -3,6 +3,7 @@ package base
|
||||
type GridLayoutFormField struct {
|
||||
Uuid string `db:"uuid"`
|
||||
GridLayoutFormUuid string `db:"grid_layout_form_uuid"` // 所属布局 UUID
|
||||
Group string `db:"group"`
|
||||
Type string `db:"type"`
|
||||
X int `db:"x"`
|
||||
Y int `db:"y"`
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.fsdpf.net/go/contracts"
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/req"
|
||||
"github.com/samber/do/v2"
|
||||
)
|
||||
|
||||
type PkgVersion struct {
|
||||
Id int64 `db:"id"`
|
||||
Name string `db:"pkg"`
|
||||
Bundle string `db:"bundle"`
|
||||
Hash string `db:"hash"`
|
||||
FileName string `db:"filename"`
|
||||
SourceMapFileName string `db:"sourcemapFileName"`
|
||||
Imports []string `db:"imports"`
|
||||
Platform string `db:"platform"`
|
||||
ReleaseAt string `db:"release_at"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
func (this PkgVersion) GetFilePath(prefix ...string) string {
|
||||
return filepath.Join(
|
||||
filepath.Join(prefix...),
|
||||
this.Platform,
|
||||
strings.Replace(this.FileName, ".", fmt.Sprintf("-%s.", this.Hash), 1),
|
||||
)
|
||||
}
|
||||
|
||||
func (this PkgVersion) GetSourceMapPath(prefix ...string) string {
|
||||
return filepath.Join(
|
||||
filepath.Join(prefix...),
|
||||
this.Platform,
|
||||
strings.Replace(this.SourceMapFileName, ".", fmt.Sprintf("-%s.", this.Hash), 1),
|
||||
)
|
||||
}
|
||||
|
||||
func (this PkgVersion) GetFile(app do.Injector, u req.User) (file string, err error) {
|
||||
if this.Id == 0 {
|
||||
return file, contracts.ErrPkgUnknown
|
||||
}
|
||||
|
||||
res, ok := do.MustInvoke[contracts.GetResource](app)("PkgVersion")
|
||||
|
||||
if !ok {
|
||||
return file, contracts.ErrResNotFound
|
||||
}
|
||||
|
||||
ok, err = res.GetDBTable(u).Where(db.C("id").Eq(this.Id)).Select("file").ScanVal(&file)
|
||||
|
||||
if !ok {
|
||||
return file, contracts.ErrResNotFound
|
||||
}
|
||||
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (this PkgVersion) GetSourceMap(app do.Injector) (file string, err error) {
|
||||
if this.Id == 0 {
|
||||
return file, contracts.ErrPkgUnknown
|
||||
}
|
||||
|
||||
// res, ok := do.MustInvoke[contracts.GetResource](app)("PkgVersion")
|
||||
|
||||
// if !ok {
|
||||
// return file, contracts.ErrResNotFound
|
||||
// }
|
||||
|
||||
// _, err = res.GetDBTable().Where("id", this.Id).Value(&file, "sourcemap")
|
||||
|
||||
return file, err
|
||||
}
|
||||
|
||||
type GetPkgVersion func(pkg string, opts ...PkgOption) (PkgVersion, bool)
|
||||
|
||||
// 筛选选项
|
||||
type PkgOption func(option *PkgOptions)
|
||||
type PkgOptions struct {
|
||||
platform string
|
||||
hash string
|
||||
}
|
||||
|
||||
func (this PkgOptions) Platform() string {
|
||||
return this.platform
|
||||
}
|
||||
|
||||
func (this PkgOptions) Hash() string {
|
||||
return this.hash
|
||||
}
|
||||
|
||||
func PkgPlatform(value string) PkgOption {
|
||||
return func(option *PkgOptions) {
|
||||
option.platform = value
|
||||
}
|
||||
}
|
||||
|
||||
func PkgHash(value string) PkgOption {
|
||||
return func(option *PkgOptions) {
|
||||
option.hash = value
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"git.fsdpf.net/go/contracts/res_type"
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
type QueryField struct {
|
||||
ResField
|
||||
typ req.RouteParamType `db:"dataType"`
|
||||
alias string `db:"alias"`
|
||||
isExpr bool `db:"isExpr"`
|
||||
isOmitempty bool
|
||||
ignored bool
|
||||
}
|
||||
|
||||
func (this QueryField) Alias() string {
|
||||
return this.alias
|
||||
}
|
||||
|
||||
func (this QueryField) Ignored() bool {
|
||||
return this.ignored
|
||||
}
|
||||
|
||||
func (this QueryField) IsExpr() bool {
|
||||
return this.isExpr
|
||||
}
|
||||
|
||||
func (this QueryField) IsOmitempty() bool {
|
||||
return this.isOmitempty
|
||||
}
|
||||
|
||||
func (this QueryField) Type() req.RouteParamType {
|
||||
return this.typ
|
||||
}
|
||||
|
||||
func (this *QueryField) SetOptions(options byte) req.QueryField {
|
||||
if options&req.IsExpr != 0 {
|
||||
this.isExpr = true
|
||||
}
|
||||
|
||||
if options&req.IsOmitempty != 0 {
|
||||
this.isOmitempty = true
|
||||
}
|
||||
|
||||
if options&req.Ignored != 0 {
|
||||
this.ignored = true
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
func (this QueryField) GetCodeOrAlias() string {
|
||||
if this.Alias() != "" {
|
||||
return this.Alias()
|
||||
}
|
||||
|
||||
return this.Code
|
||||
}
|
||||
|
||||
func (this *QueryField) ToSql() db.Expression {
|
||||
if this.Alias() != "" {
|
||||
if this.IsExpr() {
|
||||
if this.GetCode() == "" {
|
||||
return db.Raw("null as `" + this.Alias() + "`")
|
||||
}
|
||||
return db.Raw("(" + this.GetCode() + ") as `" + this.Alias() + "`")
|
||||
}
|
||||
return db.Raw("`" + this.GetCodeResource() + "`.`" + this.GetCode() + "` as `" + this.Alias() + "`")
|
||||
}
|
||||
return db.Raw("`" + this.GetCodeResource() + "`.`" + this.GetCode() + "`")
|
||||
}
|
||||
|
||||
func (this *QueryField) ToStructField(tags ...string) reflect.StructField {
|
||||
var typ reflect.Type
|
||||
|
||||
fCode := this.GetCodeOrAlias()
|
||||
|
||||
// 字段规则
|
||||
if !unicode.IsLetter(rune(fCode[0])) {
|
||||
panic("struct field name invalid. " + fCode)
|
||||
}
|
||||
|
||||
fTag := `db:"` + fCode
|
||||
|
||||
if this.ignored {
|
||||
fTag = fTag + `" json:"-"`
|
||||
} else if this.isOmitempty {
|
||||
fTag = fTag + `" json:"` + fCode + `,omitempty"`
|
||||
} else {
|
||||
fTag = fTag + `" json:"` + fCode + `"`
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
fTag = strings.Join(tags, " ") + " " + fTag
|
||||
}
|
||||
|
||||
switch this.typ {
|
||||
case req.ReqString:
|
||||
typ = reflect.TypeOf(string(""))
|
||||
case req.ReqNumber:
|
||||
typ = reflect.TypeOf(res_type.ResFieldByNumber(0))
|
||||
case req.ReqInteger:
|
||||
typ = reflect.TypeOf(res_type.ResFieldByInteger(0))
|
||||
case req.ReqFloat:
|
||||
typ = reflect.TypeOf(res_type.ResFieldByFloat(0))
|
||||
case req.ReqBool:
|
||||
typ = reflect.TypeOf(true)
|
||||
case req.ReqArray:
|
||||
typ = reflect.TypeOf([]any{})
|
||||
case req.ReqJson:
|
||||
typ = reflect.TypeOf(map[string]any{})
|
||||
}
|
||||
|
||||
return reflect.StructField{
|
||||
Name: strings.ToUpper(fCode[:1]) + fCode[1:],
|
||||
Tag: reflect.StructTag(fTag),
|
||||
Type: typ,
|
||||
}
|
||||
}
|
||||
+6
-9
@@ -1,22 +1,17 @@
|
||||
package base
|
||||
|
||||
type ResApiParam struct {
|
||||
Code string `db:"code"`
|
||||
Name string `db:"name"`
|
||||
Category string `db:"category"`
|
||||
DataType string `db:"type"`
|
||||
IsRequired bool `db:"isRequired"`
|
||||
}
|
||||
|
||||
type ResApi struct {
|
||||
Uuid string `db:"uuid"`
|
||||
PUuid string `db:"pUuid"`
|
||||
Code string `db:"code"`
|
||||
UriPrefix string `db:"uri_prefix"`
|
||||
Name string `db:"name"`
|
||||
Desc string `db:"desc"`
|
||||
PrimaryKey string `db:"primaryKey"`
|
||||
ResourceUuid string `db:"resource_uuid"`
|
||||
Method string `db:"method"`
|
||||
Category string `db:"action"`
|
||||
Action string `db:"action"`
|
||||
Version string `db:"version"`
|
||||
Params []ResApiParam `db:"params"`
|
||||
Roles []string `db:"roles"`
|
||||
IsAuthDB int `db:"is_auth_db"` // 启用权限过滤
|
||||
@@ -25,3 +20,5 @@ type ResApi struct {
|
||||
}
|
||||
|
||||
type GetResApi func(code string) (ResApi, bool)
|
||||
|
||||
type GetResApiChilds func(code string) []ResApi
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
jschema "github.com/google/jsonschema-go/jsonschema"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type ResApiParam struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
PID int64 `db:"pid" json:"pid"`
|
||||
Code string `db:"code" json:"code"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Category string `db:"category" json:"category"`
|
||||
DataType string `db:"type" json:"type"`
|
||||
Desc string `db:"desc" json:"desc"`
|
||||
IsRequired bool `db:"isRequired" json:"isRequired"`
|
||||
Constraints map[string]any `db:"constraints" json:"constraints"`
|
||||
Rank int `db:"rank" json:"rank"`
|
||||
RefID int64 `db:"ref_id" json:"ref_id"`
|
||||
DefaultVal string `db:"df_val" json:"df_val"`
|
||||
}
|
||||
|
||||
type ResApiParams []ResApiParam
|
||||
|
||||
// ToJSONSchema 将扁平参数列表还原为 JSON Schema 对象
|
||||
func (params ResApiParams) ToJSONSchema() *jschema.Schema {
|
||||
return params.buildObject(0)
|
||||
}
|
||||
|
||||
// ResApiParamsFromJSONSchema 将 JSON Schema 对象解析为扁平参数列表
|
||||
func ResApiParamsFromJSONSchema(schema *jschema.Schema) ResApiParams {
|
||||
var params ResApiParams
|
||||
var seq int64
|
||||
params.parseObject(schema, 0, &seq)
|
||||
return params
|
||||
}
|
||||
|
||||
func (params ResApiParams) childrenOf(pid int64) ResApiParams {
|
||||
var result ResApiParams
|
||||
for _, p := range params {
|
||||
if p.PID == pid {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Rank < result[j].Rank
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (params ResApiParams) buildObject(pid int64) *jschema.Schema {
|
||||
s := &jschema.Schema{Type: "object"}
|
||||
props := map[string]*jschema.Schema{}
|
||||
|
||||
for _, p := range params.childrenOf(pid) {
|
||||
if p.Code == "items" || p.Code == "oneOf" {
|
||||
continue
|
||||
}
|
||||
props[p.Code] = params.buildProperty(p)
|
||||
if p.IsRequired {
|
||||
s.Required = append(s.Required, p.Code)
|
||||
}
|
||||
}
|
||||
if len(props) > 0 {
|
||||
s.Properties = props
|
||||
}
|
||||
sort.Strings(s.Required)
|
||||
return s
|
||||
}
|
||||
|
||||
func (params ResApiParams) findByID(id int64) (ResApiParam, bool) {
|
||||
for _, p := range params {
|
||||
if p.ID == id {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return ResApiParam{}, false
|
||||
}
|
||||
|
||||
// pathOf 计算参数在生成的 JSON Schema 中的路径
|
||||
func (params ResApiParams) pathOf(id int64) string {
|
||||
p, ok := params.findByID(id)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if p.PID == 0 {
|
||||
return "#/properties/" + p.Code
|
||||
}
|
||||
parent := params.pathOf(p.PID)
|
||||
switch p.Code {
|
||||
case "items":
|
||||
return parent + "/items"
|
||||
case "oneOf":
|
||||
return parent + "/items/oneOf/" + strconv.Itoa(p.Rank)
|
||||
default:
|
||||
return parent + "/properties/" + p.Code
|
||||
}
|
||||
}
|
||||
|
||||
func (params ResApiParams) buildProperty(p ResApiParam) *jschema.Schema {
|
||||
if p.RefID != 0 {
|
||||
if _, ok := params.findByID(p.RefID); ok {
|
||||
return &jschema.Schema{Ref: params.pathOf(p.RefID), Description: p.Desc}
|
||||
}
|
||||
}
|
||||
|
||||
// any 类型:不设 type,从 oneOf 子节点构建
|
||||
if p.DataType == "any" {
|
||||
s := &jschema.Schema{Title: p.Name, Description: p.Desc}
|
||||
if p.DefaultVal != "" {
|
||||
var tmp any
|
||||
if err := json.Unmarshal([]byte(p.DefaultVal), &tmp); err == nil {
|
||||
s.Default = json.RawMessage(p.DefaultVal)
|
||||
} else {
|
||||
s.Default, _ = json.Marshal(p.DefaultVal)
|
||||
}
|
||||
}
|
||||
for _, child := range params.childrenOf(p.ID) {
|
||||
c := child
|
||||
if child.Code == "oneOf" {
|
||||
s.OneOf = append(s.OneOf, params.buildProperty(c))
|
||||
}
|
||||
}
|
||||
params.applyConstraints(s, p.Constraints)
|
||||
return s
|
||||
}
|
||||
|
||||
// 归一化 DataType:json → object;空字符串根据子节点推断
|
||||
dataType := p.DataType
|
||||
switch dataType {
|
||||
case "json":
|
||||
dataType = "object"
|
||||
case "bool":
|
||||
dataType = "boolean"
|
||||
case "":
|
||||
for _, c := range params.childrenOf(p.ID) {
|
||||
if c.Code == "oneOf" || c.Code == "items" {
|
||||
dataType = "array"
|
||||
break
|
||||
}
|
||||
}
|
||||
if dataType == "" && len(params.childrenOf(p.ID)) > 0 {
|
||||
dataType = "object"
|
||||
}
|
||||
}
|
||||
|
||||
s := &jschema.Schema{
|
||||
Type: dataType,
|
||||
Title: p.Name,
|
||||
Description: p.Desc,
|
||||
}
|
||||
|
||||
if p.DefaultVal != "" {
|
||||
var tmp any
|
||||
if err := json.Unmarshal([]byte(p.DefaultVal), &tmp); err == nil {
|
||||
s.Default = json.RawMessage(p.DefaultVal)
|
||||
} else {
|
||||
s.Default, _ = json.Marshal(p.DefaultVal)
|
||||
}
|
||||
}
|
||||
|
||||
params.applyConstraints(s, p.Constraints)
|
||||
|
||||
switch dataType {
|
||||
case "object":
|
||||
nested := params.buildObject(p.ID)
|
||||
s.Properties = nested.Properties
|
||||
s.Required = nested.Required
|
||||
case "array":
|
||||
var oneOfSchemas []*jschema.Schema
|
||||
var itemsChild *ResApiParam
|
||||
for _, child := range params.childrenOf(p.ID) {
|
||||
c := child
|
||||
switch child.Code {
|
||||
case "oneOf":
|
||||
oneOfSchemas = append(oneOfSchemas, params.buildProperty(c))
|
||||
case "items":
|
||||
itemsChild = &c
|
||||
}
|
||||
}
|
||||
if len(oneOfSchemas) > 0 {
|
||||
s.Items = &jschema.Schema{OneOf: oneOfSchemas}
|
||||
} else if itemsChild != nil {
|
||||
s.Items = params.buildItems(*itemsChild)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (params ResApiParams) buildItems(p ResApiParam) *jschema.Schema {
|
||||
if ref, ok := p.Constraints["$ref"].(string); ok {
|
||||
return &jschema.Schema{Ref: ref}
|
||||
}
|
||||
if p.RefID != 0 {
|
||||
if ref, ok := params.findByID(p.RefID); ok {
|
||||
path := params.pathOf(p.RefID)
|
||||
if ref.DataType == "array" {
|
||||
path += "/items"
|
||||
}
|
||||
return &jschema.Schema{Ref: path}
|
||||
}
|
||||
}
|
||||
itemType := p.DataType
|
||||
if itemType == "json" {
|
||||
itemType = "object"
|
||||
} else if itemType == "" && len(params.childrenOf(p.ID)) > 0 {
|
||||
itemType = "object"
|
||||
}
|
||||
|
||||
s := &jschema.Schema{
|
||||
Type: itemType,
|
||||
Title: p.Name,
|
||||
Description: p.Desc,
|
||||
}
|
||||
|
||||
if s.Description == "" && s.Title != "" {
|
||||
s.Description = s.Title
|
||||
}
|
||||
|
||||
if itemType == "object" {
|
||||
nested := params.buildObject(p.ID)
|
||||
s.Properties = nested.Properties
|
||||
s.Required = nested.Required
|
||||
}
|
||||
params.applyConstraints(s, p.Constraints)
|
||||
return s
|
||||
}
|
||||
|
||||
func (params ResApiParams) applyConstraints(s *jschema.Schema, c map[string]any) {
|
||||
for k, v := range c {
|
||||
switch k {
|
||||
case "$ref":
|
||||
if ref, ok := v.(string); ok {
|
||||
s.Ref = ref
|
||||
}
|
||||
case "enum":
|
||||
if arr, ok := v.([]any); ok {
|
||||
s.Enum = arr
|
||||
}
|
||||
case "format":
|
||||
if f, ok := v.(string); ok {
|
||||
s.Format = f
|
||||
}
|
||||
case "pattern":
|
||||
if p, ok := v.(string); ok {
|
||||
s.Pattern = p
|
||||
}
|
||||
case "minLength":
|
||||
s.MinLength = jschema.Ptr(cast.ToInt(v))
|
||||
case "maxLength":
|
||||
s.MaxLength = jschema.Ptr(cast.ToInt(v))
|
||||
case "minimum":
|
||||
s.Minimum = jschema.Ptr(cast.ToFloat64(v))
|
||||
case "maximum":
|
||||
s.Maximum = jschema.Ptr(cast.ToFloat64(v))
|
||||
case "multipleOf":
|
||||
s.MultipleOf = jschema.Ptr(cast.ToFloat64(v))
|
||||
case "minItems":
|
||||
s.MinItems = jschema.Ptr(cast.ToInt(v))
|
||||
case "maxItems":
|
||||
s.MaxItems = jschema.Ptr(cast.ToInt(v))
|
||||
case "minProperties":
|
||||
s.MinProperties = jschema.Ptr(cast.ToInt(v))
|
||||
case "maxProperties":
|
||||
s.MaxProperties = jschema.Ptr(cast.ToInt(v))
|
||||
case "additionalProperties":
|
||||
switch val := v.(type) {
|
||||
case bool:
|
||||
if val {
|
||||
s.AdditionalProperties = &jschema.Schema{}
|
||||
} else {
|
||||
s.AdditionalProperties = &jschema.Schema{Not: &jschema.Schema{}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (params *ResApiParams) parseObject(schema *jschema.Schema, pid int64, seq *int64) {
|
||||
requiredSet := make(map[string]bool, len(schema.Required))
|
||||
for _, r := range schema.Required {
|
||||
requiredSet[r] = true
|
||||
}
|
||||
|
||||
rank := 0
|
||||
for code, prop := range schema.Properties {
|
||||
*seq++
|
||||
id := *seq
|
||||
|
||||
p := ResApiParam{
|
||||
ID: id,
|
||||
PID: pid,
|
||||
Code: code,
|
||||
DataType: prop.Type,
|
||||
Name: prop.Title,
|
||||
Desc: prop.Description,
|
||||
IsRequired: requiredSet[code],
|
||||
DefaultVal: params.rawToStr(prop.Default),
|
||||
Rank: rank,
|
||||
Constraints: params.extractConstraints(prop),
|
||||
}
|
||||
*params = append(*params, p)
|
||||
rank++
|
||||
|
||||
switch prop.Type {
|
||||
case "object":
|
||||
params.parseObject(prop, id, seq)
|
||||
case "array":
|
||||
if prop.Items == nil {
|
||||
break
|
||||
}
|
||||
// items 为纯 oneOf schema:建立 oneOf 子行,不建 items 子行
|
||||
if len(prop.Items.OneOf) > 0 {
|
||||
for oneOfRank, sub := range prop.Items.OneOf {
|
||||
*seq++
|
||||
oneOfID := *seq
|
||||
itemType := sub.Type
|
||||
if itemType == "" {
|
||||
itemType = "object"
|
||||
}
|
||||
*params = append(*params, ResApiParam{
|
||||
ID: oneOfID,
|
||||
PID: id,
|
||||
Code: "oneOf",
|
||||
DataType: itemType,
|
||||
Name: sub.Title,
|
||||
Desc: sub.Description,
|
||||
Rank: oneOfRank,
|
||||
Constraints: params.extractConstraints(sub),
|
||||
})
|
||||
if itemType == "object" {
|
||||
params.parseObject(sub, oneOfID, seq)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
*seq++
|
||||
itemID := *seq
|
||||
itemType := prop.Items.Type
|
||||
if itemType == "" {
|
||||
itemType = "object"
|
||||
}
|
||||
*params = append(*params, ResApiParam{
|
||||
ID: itemID,
|
||||
PID: id,
|
||||
Code: "items",
|
||||
DataType: itemType,
|
||||
Name: prop.Items.Title,
|
||||
Desc: prop.Items.Description,
|
||||
Constraints: params.extractConstraints(prop.Items),
|
||||
})
|
||||
if itemType == "object" {
|
||||
params.parseObject(prop.Items, itemID, seq)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (params ResApiParams) extractConstraints(s *jschema.Schema) map[string]any {
|
||||
c := map[string]any{}
|
||||
if s.Ref != "" {
|
||||
c["$ref"] = s.Ref
|
||||
}
|
||||
if len(s.Enum) > 0 {
|
||||
c["enum"] = s.Enum
|
||||
}
|
||||
if s.Format != "" {
|
||||
c["format"] = s.Format
|
||||
}
|
||||
if s.Pattern != "" {
|
||||
c["pattern"] = s.Pattern
|
||||
}
|
||||
if s.MinLength != nil {
|
||||
c["minLength"] = *s.MinLength
|
||||
}
|
||||
if s.MaxLength != nil {
|
||||
c["maxLength"] = *s.MaxLength
|
||||
}
|
||||
if s.Minimum != nil {
|
||||
c["minimum"] = *s.Minimum
|
||||
}
|
||||
if s.Maximum != nil {
|
||||
c["maximum"] = *s.Maximum
|
||||
}
|
||||
if s.MultipleOf != nil {
|
||||
c["multipleOf"] = *s.MultipleOf
|
||||
}
|
||||
if s.MinItems != nil {
|
||||
c["minItems"] = *s.MinItems
|
||||
}
|
||||
if s.MaxItems != nil {
|
||||
c["maxItems"] = *s.MaxItems
|
||||
}
|
||||
if s.MinProperties != nil {
|
||||
c["minProperties"] = *s.MinProperties
|
||||
}
|
||||
if s.MaxProperties != nil {
|
||||
c["maxProperties"] = *s.MaxProperties
|
||||
}
|
||||
if s.AdditionalProperties != nil {
|
||||
c["additionalProperties"] = s.AdditionalProperties.Not == nil
|
||||
}
|
||||
if len(c) == 0 {
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (params ResApiParams) rawToStr(raw json.RawMessage) string {
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(raw, &s); err == nil {
|
||||
return s
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
package base_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/contracts/base"
|
||||
jschema "github.com/google/jsonschema-go/jsonschema"
|
||||
)
|
||||
|
||||
// queryParams 查询接口的扁平参数列表,包含 conditions(过滤条件)和 select(查询字段)
|
||||
var queryParams = base.ResApiParams{
|
||||
// resource:目标资源
|
||||
{
|
||||
ID: -1, PID: 0, Code: "resource", DataType: "string", Name: "资源", Rank: 0,
|
||||
Desc: "查询目标资源编码(resource_code)",
|
||||
},
|
||||
{
|
||||
ID: 1, PID: 0, Code: "conditions", DataType: "object", Name: "过滤条件", Rank: 1,
|
||||
Desc: `WHERE 条件树。
|
||||
exprs 每项生成一段比较:[{columnSqlFunc}(]{columnResource}.{column}[)] {operator} {value},同层多项以 type(and/or) 连接;
|
||||
children 为同结构子节点,递归嵌套实现复杂过滤。`,
|
||||
},
|
||||
{
|
||||
ID: 2, PID: 1, Code: "type", DataType: "string", Name: "逻辑运算符", Rank: 0,
|
||||
Desc: "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
|
||||
DefaultVal: "and",
|
||||
Constraints: map[string]any{"enum": []any{"and", "or"}},
|
||||
},
|
||||
{
|
||||
ID: 3, PID: 1, Code: "exprs", DataType: "array", Name: "条件表达式列表", Rank: 1,
|
||||
Desc: "当前层的叶子比较列表,每项生成一个 column {operator} value 片段,同层多项以 type 连接",
|
||||
},
|
||||
{ID: 4, PID: 3, Code: "items", DataType: "object", Rank: 0},
|
||||
{
|
||||
ID: 5, PID: 4, Code: "column", DataType: "string", Name: "字段", Rank: 0,
|
||||
Desc: "字段编码", IsRequired: true,
|
||||
},
|
||||
{
|
||||
ID: 6, PID: 4, Code: "columnResource", DataType: "string", Name: "字段资源", Rank: 1,
|
||||
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
},
|
||||
{
|
||||
ID: 7, PID: 4, Code: "operator", DataType: "string", Name: "运算符", Rank: 2,
|
||||
Desc: `标准 SQL 比较运算符:
|
||||
= / != / > / >= / < / <=
|
||||
LIKE:模糊匹配(%keyword%)
|
||||
IN:包含,value 逗号分隔多值
|
||||
IS NULL / IS NOT NULL:空值判断,无需 value
|
||||
REGEXP:正则匹配`,
|
||||
IsRequired: true,
|
||||
DefaultVal: "=",
|
||||
},
|
||||
{
|
||||
ID: 8, PID: 4, Code: "value", DataType: "string", Name: "比较值", Rank: 3,
|
||||
Desc: "比较目标,内容由 valueType 决定。operator 为 IS NULL/IS NOT NULL 时可省略",
|
||||
},
|
||||
{
|
||||
ID: 9, PID: 4, Code: "valueType", DataType: "string", Name: "值类型", Rank: 4,
|
||||
Desc: `先选类型再填 value:
|
||||
string:value 填字面量如 '2024-01-01'
|
||||
func:value 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS
|
||||
param:value 填请求参数路径如 body.status
|
||||
sql:value 填原始SQL如 CURRENT_DATE
|
||||
默认 string`,
|
||||
Constraints: map[string]any{"enum": []any{"string", "func", "sql", "param"}},
|
||||
},
|
||||
{
|
||||
ID: 10, PID: 4, Code: "columnSqlFunc", DataType: "string", Name: "列SQL函数", Rank: 5,
|
||||
Desc: `对列应用 SQL 函数后再参与比较,拼接为 {func}({columnResource}.{column}) {operator} {value}。
|
||||
支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。
|
||||
需要第二参数的函数(如 ROUND、CAST)配合 columnSqlFuncParam 使用。`,
|
||||
},
|
||||
{
|
||||
ID: 13, PID: 4, Code: "columnSqlFuncParam", DataType: "string", Name: "SQL函数参数", Rank: 6,
|
||||
Desc: `columnSqlFunc 的附加参数,按函数类型用途不同:
|
||||
|
||||
通用函数(有 param 时):{func}({columnResource}.{column}, {columnSqlFuncParam}) {operator} {value}
|
||||
示例:ROUND(col, 2) → columnSqlFuncParam="2"
|
||||
SUBSTRING(col, 1, 10) → columnSqlFuncParam="1, 10"
|
||||
|
||||
JSON 函数(param 为 JSON 路径表达式):
|
||||
json_member_of(value 是否是列 JSON 数组的成员):
|
||||
无 param:JSON_CONTAINS({value}, JSON_ARRAY({column}))
|
||||
有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({column}))
|
||||
json_contains(列 JSON 是否包含 value):
|
||||
无 param:JSON_CONTAINS({column}, JSON_ARRAY({value}))
|
||||
有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({value}))`,
|
||||
},
|
||||
{
|
||||
ID: 14, PID: 4, Code: "ignoreEmptyParam", DataType: "boolean", Name: "忽略空参数", Rank: 7,
|
||||
Desc: "当 valueType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项,默认 false",
|
||||
},
|
||||
{
|
||||
ID: 11, PID: 1, Code: "children", DataType: "array", Name: "子条件", Rank: 2,
|
||||
Desc: "子条件节点,递归结构",
|
||||
},
|
||||
{ID: 12, PID: 11, Code: "items", DataType: "object", RefID: 1, Rank: 0},
|
||||
// fields 用 oneOf 子行表示
|
||||
{
|
||||
ID: 15, PID: 0, Code: "fields", DataType: "array", Name: "查询字段", Rank: 2,
|
||||
Desc: "查询字段,不传则返回所有字段",
|
||||
},
|
||||
{
|
||||
ID: 55, PID: 15, Code: "oneOf", DataType: "string", Name: "字段", Rank: 0,
|
||||
Desc: "直接填字段编码,默认归属于顶层 resource。等价于 {column: \"field\", columnResource: resource}",
|
||||
},
|
||||
{ID: 16, PID: 15, Code: "oneOf", DataType: "object", Name: "字段引用", Rank: 1},
|
||||
{ID: 44, PID: 16, Code: "column", DataType: "string", Name: "字段", Rank: 0, IsRequired: true},
|
||||
{
|
||||
ID: 46, PID: 16, Code: "columnResource", DataType: "string", Name: "字段资源", Rank: 1,
|
||||
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
},
|
||||
{ID: 45, PID: 16, Code: "alias", DataType: "string", Name: "结果别名", Rank: 2},
|
||||
{ID: 17, PID: 15, Code: "oneOf", DataType: "object", Name: "表达式字段", Rank: 2},
|
||||
{ID: 18, PID: 17, Code: "expr", DataType: "string", Name: "SQL 表达式", Rank: 0, IsRequired: true,
|
||||
Desc: "{column} / {columnResource}.{column} / SQL 表达式"},
|
||||
{ID: 19, PID: 17, Code: "alias", DataType: "string", Name: "结果别名", Rank: 1},
|
||||
|
||||
// orderBy:items 用 oneOf 表示三种形式,每个变体含 direction
|
||||
{
|
||||
ID: 20, PID: 0, Code: "orderBy", DataType: "array", Name: "排序规则", Rank: 3,
|
||||
Desc: "排序规则列表,多项按顺序依次应用",
|
||||
},
|
||||
|
||||
{ID: 48, PID: 20, Code: "oneOf", DataType: "object", Name: "资源字段排序", Rank: 1},
|
||||
{ID: 49, PID: 48, Code: "column", DataType: "string", Name: "字段", Rank: 0, IsRequired: true},
|
||||
{
|
||||
ID: 50, PID: 48, Code: "columnResource", DataType: "string", Name: "字段资源", Rank: 1,
|
||||
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
},
|
||||
{
|
||||
ID: 53, PID: 48, Code: "direction", RefID: 56,
|
||||
},
|
||||
|
||||
{ID: 51, PID: 20, Code: "oneOf", DataType: "object", Name: "SQL 表达式排序", Rank: 2},
|
||||
{ID: 52, PID: 51, Code: "expr", DataType: "string", Name: "字段表达式", Rank: 0, IsRequired: true},
|
||||
{
|
||||
ID: 54, PID: 51, Code: "direction", RefID: 56,
|
||||
},
|
||||
|
||||
{
|
||||
ID: 57, PID: 20, Code: "oneOf", DataType: "string", Name: "SQL 表达式 (ASC)", Rank: 0,
|
||||
Desc: "直接填字段编码,默认升序(asc)",
|
||||
},
|
||||
// groupBy:分组字段
|
||||
{
|
||||
ID: 24, PID: 0, Code: "groupBy", DataType: "array", Name: "分组规则", Rank: 4,
|
||||
Desc: "GROUP BY 字段列表,配合 fields 中的聚合字段使用",
|
||||
},
|
||||
{
|
||||
ID: 58, PID: 24, Code: "oneOf", DataType: "string", Name: "分组表达式", Desc: "{column} / {columnResource}.{column} / SQL 表达式", Rank: 0,
|
||||
},
|
||||
{
|
||||
ID: 59, PID: 24, Code: "oneOf", DataType: "object", Name: "资源字段分组", Rank: 0,
|
||||
},
|
||||
{ID: 60, PID: 59, Code: "column", DataType: "string", Name: "字段编码", Rank: 0, IsRequired: true},
|
||||
{
|
||||
ID: 61, PID: 59, Code: "columnResource", DataType: "string", Name: "所属资源", Rank: 1,
|
||||
Desc: "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
},
|
||||
// limit / offset:分页
|
||||
{
|
||||
ID: 26, PID: 0, Code: "limit", DataType: "integer", Name: "每页条数", Rank: 5,
|
||||
Desc: "返回的最大记录数,默认 30", DefaultVal: "30", Constraints: map[string]any{"maximum": 50},
|
||||
},
|
||||
{
|
||||
ID: 27, PID: 0, Code: "offset", DataType: "integer", Name: "偏移量", Rank: 6,
|
||||
Desc: "跳过的记录数,默认 0",
|
||||
},
|
||||
// relations:资源关联
|
||||
{
|
||||
ID: 28, PID: 0, Code: "relations", DataType: "array", Name: "资源关联", Rank: 7,
|
||||
Desc: "关联配置列表。\ninner / left / right:SQL JOIN,要求关联资源与主资源在同一数据库连接,将关联字段合并到主查询行。\nhasOne / hasMany:独立子查询,支持跨数据库连接,结果聚合为 JSON 字段(hasOne 为单对象,hasMany 为数组)。\n跨库关联必须使用 hasOne 或 hasMany,不可使用 JOIN 类型。",
|
||||
},
|
||||
{ID: 29, PID: 28, Code: "items", DataType: "object", Rank: 0},
|
||||
{
|
||||
ID: 30, PID: 29, Code: "code", DataType: "string", Name: "关联标识", Rank: 0,
|
||||
Desc: "关联唯一标识,同时作为别名:JOIN 时为表别名(AS code),子查询时为结果 JSON 字段的键名",
|
||||
IsRequired: true,
|
||||
},
|
||||
{
|
||||
ID: 38, PID: 29, Code: "name", DataType: "string", Name: "关联名称", Rank: 1,
|
||||
Desc: "关联的显示名称",
|
||||
},
|
||||
{
|
||||
ID: 39, PID: 29, Code: "type", DataType: "string", Name: "关联类型", Rank: 2,
|
||||
Desc: `关联模式:
|
||||
inner / left / right:SQL JOIN,要求与主资源同一数据库连接,将关联字段合并到主查询行
|
||||
hasOne:独立子查询,支持跨库,结果聚合为单个对象(result[code] = {})
|
||||
hasMany:独立子查询,支持跨库,结果聚合为对象数组(result[code] = [...])
|
||||
跨数据库连接的关联必须使用 hasOne 或 hasMany`,
|
||||
Constraints: map[string]any{"enum": []any{"inner", "left", "right", "hasOne", "hasMany"}},
|
||||
},
|
||||
{ID: 40, PID: 29, Code: "actuallyResource", DataType: "string", Name: "实际资源", Rank: 3,
|
||||
Desc: "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}"},
|
||||
{ID: 41, PID: 29, Code: "actuallyField", DataType: "string", Name: "实际资源字段", Rank: 4,
|
||||
Desc: "目标资源(code)参与 ON 条件的字段"},
|
||||
{ID: 42, PID: 29, Code: "relationResource", DataType: "string", Name: "被关联资源", Rank: 5,
|
||||
Desc: "ON 条件对端的资源编码(通常为主资源或父关联资源)"},
|
||||
{ID: 43, PID: 29, Code: "relationField", DataType: "string", Name: "被关联字段", Rank: 6,
|
||||
Desc: "ON 条件对端资源的字段"},
|
||||
{ID: 31, PID: 29, Code: "conditions", RefID: 1, Rank: 7, Desc: "关联的过滤条件"},
|
||||
{ID: 33, PID: 29, Code: "orderBy", RefID: 20, Rank: 8, Desc: "关联排序,仅 hasOne/hasMany 有效"},
|
||||
{ID: 34, PID: 29, Code: "groupBy", RefID: 24, Rank: 9, Desc: "关联分组,仅 hasOne/hasMany 有效"},
|
||||
{ID: 35, PID: 29, Code: "limit", RefID: 26, Rank: 10, Desc: "关联条数限制,仅 hasOne/hasMany 有效"},
|
||||
{ID: 36, PID: 29, Code: "offset", RefID: 27, Rank: 11, Desc: "关联偏移量,仅 hasOne/hasMany 有效"},
|
||||
}
|
||||
|
||||
func TestResApiParams_PrintSchema(t *testing.T) {
|
||||
b, _ := json.MarshalIndent(queryParams.ToJSONSchema(), "", " ")
|
||||
t.Log("\n" + string(b))
|
||||
}
|
||||
|
||||
func TestResApiParams_ToJSONSchema(t *testing.T) {
|
||||
schema := queryParams.ToJSONSchema()
|
||||
|
||||
if schema.Type != "object" {
|
||||
t.Fatalf("expected type=object, got %s", schema.Type)
|
||||
}
|
||||
|
||||
cond, ok := schema.Properties["conditions"]
|
||||
if !ok {
|
||||
t.Fatal("missing property: conditions")
|
||||
}
|
||||
if cond.Type != "object" {
|
||||
t.Errorf("conditions: expected type=object, got %s", cond.Type)
|
||||
}
|
||||
if cond.Description != "WHERE 条件树。\nexprs 每项生成一段比较:[{columnSqlFunc}(]{columnResource}.{column}[)] {operator} {value},同层多项以 type(and/or) 连接;\nchildren 为同结构子节点,递归嵌套实现复杂过滤。" {
|
||||
t.Errorf("conditions: unexpected description: %s", cond.Description)
|
||||
}
|
||||
|
||||
// conditions.type 枚举和默认值
|
||||
typeProp, ok := cond.Properties["type"]
|
||||
if !ok {
|
||||
t.Fatal("missing property: conditions.type")
|
||||
}
|
||||
if len(typeProp.Enum) != 2 {
|
||||
t.Errorf("conditions.type: expected 2 enum values, got %d", len(typeProp.Enum))
|
||||
}
|
||||
var defaultVal string
|
||||
if err := json.Unmarshal(typeProp.Default, &defaultVal); err != nil || defaultVal != "and" {
|
||||
t.Errorf("conditions.type: expected default=and, got %s", typeProp.Default)
|
||||
}
|
||||
|
||||
// conditions.exprs 是 array,items 是 object
|
||||
exprsProp, ok := cond.Properties["exprs"]
|
||||
if !ok {
|
||||
t.Fatal("missing property: conditions.exprs")
|
||||
}
|
||||
if exprsProp.Type != "array" {
|
||||
t.Errorf("conditions.exprs: expected type=array, got %s", exprsProp.Type)
|
||||
}
|
||||
if exprsProp.Items == nil {
|
||||
t.Fatal("conditions.exprs: missing items")
|
||||
}
|
||||
if exprsProp.Items.Type != "object" {
|
||||
t.Errorf("conditions.exprs.items: expected type=object, got %s", exprsProp.Items.Type)
|
||||
}
|
||||
|
||||
// exprs.items required: column, operator
|
||||
requiredSet := map[string]bool{}
|
||||
for _, r := range exprsProp.Items.Required {
|
||||
requiredSet[r] = true
|
||||
}
|
||||
if !requiredSet["column"] || !requiredSet["operator"] {
|
||||
t.Errorf("conditions.exprs.items: expected required=[column, operator], got %v", exprsProp.Items.Required)
|
||||
}
|
||||
|
||||
// children.items $ref
|
||||
childrenProp, ok := cond.Properties["children"]
|
||||
if !ok {
|
||||
t.Fatal("missing property: conditions.children")
|
||||
}
|
||||
if childrenProp.Items == nil {
|
||||
t.Fatal("conditions.children: missing items")
|
||||
}
|
||||
if childrenProp.Items.Ref != "#/properties/conditions" {
|
||||
t.Errorf("conditions.children.items: expected $ref=#/properties/conditions, got %s", childrenProp.Items.Ref)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResApiParamsFromJSONSchema(t *testing.T) {
|
||||
original := queryParams.ToJSONSchema()
|
||||
params := base.ResApiParamsFromJSONSchema(original)
|
||||
|
||||
if len(params) == 0 {
|
||||
t.Fatal("ResApiParamsFromJSONSchema returned empty params")
|
||||
}
|
||||
|
||||
// 转回 schema 后结构应一致
|
||||
rebuilt := params.ToJSONSchema()
|
||||
|
||||
// 将 original 和 rebuilt 都序列化为 map[string]any 后 DeepEqual 比较
|
||||
toMap := func(s *jschema.Schema) map[string]any {
|
||||
b, _ := json.Marshal(s)
|
||||
var m map[string]any
|
||||
json.Unmarshal(b, &m)
|
||||
return m
|
||||
}
|
||||
originalMap := toMap(original)
|
||||
rebuiltMap := toMap(rebuilt)
|
||||
|
||||
if !reflect.DeepEqual(originalMap, rebuiltMap) {
|
||||
ob, _ := json.MarshalIndent(originalMap, "", " ")
|
||||
rb, _ := json.MarshalIndent(rebuiltMap, "", " ")
|
||||
t.Errorf("schema mismatch:\noriginal: %s\nrebuilt: %s", ob, rb)
|
||||
}
|
||||
}
|
||||
|
||||
// createParams res-create 接口参数,data 字段为 any 类型(oneOf object | array<object>)
|
||||
var createParams = base.ResApiParams{
|
||||
{ID: 315, PID: 0, Code: "resource", DataType: "string", Name: "主资源", Rank: 1,
|
||||
Desc: "目标资源编码,数据将写入该资源对应的表"},
|
||||
{ID: 316, PID: 0, Code: "data", DataType: "any", Name: "创建数据", Rank: 2,
|
||||
Desc: "单条记录传对象,批量创建传对象数组"},
|
||||
// oneOf 分支1:单条记录(object + additionalProperties: true)
|
||||
{ID: 317, PID: 316, Code: "oneOf", DataType: "json", Name: "单条记录", Rank: 1,
|
||||
Desc: "创建单条记录,key 为字段编码,value 为字段值",
|
||||
Constraints: map[string]any{"additionalProperties": true}},
|
||||
// oneOf 分支2:批量记录(array,items 为 object + additionalProperties: true)
|
||||
{ID: 318, PID: 316, Code: "oneOf", DataType: "array", Name: "批量记录", Rank: 2,
|
||||
Desc: "批量创建,每个元素为一条记录对象"},
|
||||
{ID: 319, PID: 318, Code: "items", DataType: "json", Name: "记录对象", Rank: 1,
|
||||
Desc: "批量创建,每个元素为一条记录对象",
|
||||
Constraints: map[string]any{"additionalProperties": true}},
|
||||
}
|
||||
|
||||
func TestResApiParams_AnyType(t *testing.T) {
|
||||
schema := createParams.ToJSONSchema()
|
||||
|
||||
// data 字段存在
|
||||
data, ok := schema.Properties["data"]
|
||||
if !ok {
|
||||
t.Fatal("missing property: data")
|
||||
}
|
||||
|
||||
// any 类型不应有 type
|
||||
if data.Type != "" {
|
||||
t.Errorf("data: expected empty type for any, got %q", data.Type)
|
||||
}
|
||||
|
||||
// oneOf 应有 2 个分支
|
||||
if len(data.OneOf) != 2 {
|
||||
t.Fatalf("data.oneOf: expected 2 branches, got %d", len(data.OneOf))
|
||||
}
|
||||
|
||||
// 分支1:object
|
||||
branch1 := data.OneOf[0]
|
||||
if branch1.Type != "object" {
|
||||
t.Errorf("data.oneOf[0]: expected type=object, got %q", branch1.Type)
|
||||
}
|
||||
if branch1.AdditionalProperties == nil {
|
||||
t.Error("data.oneOf[0]: expected additionalProperties to be set")
|
||||
}
|
||||
|
||||
// 分支2:array,items 为 object
|
||||
branch2 := data.OneOf[1]
|
||||
if branch2.Type != "array" {
|
||||
t.Errorf("data.oneOf[1]: expected type=array, got %q", branch2.Type)
|
||||
}
|
||||
if branch2.Items == nil {
|
||||
t.Fatal("data.oneOf[1]: missing items")
|
||||
}
|
||||
if branch2.Items.Type != "object" {
|
||||
t.Errorf("data.oneOf[1].items: expected type=object, got %q", branch2.Items.Type)
|
||||
}
|
||||
if branch2.Items.AdditionalProperties == nil {
|
||||
t.Error("data.oneOf[1].items: expected additionalProperties to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResApiParams_AdditionalProperties(t *testing.T) {
|
||||
params := base.ResApiParams{
|
||||
{ID: 1, PID: 0, Code: "open", DataType: "json", Name: "开放对象",
|
||||
Constraints: map[string]any{"additionalProperties": true}},
|
||||
{ID: 2, PID: 0, Code: "strict", DataType: "json", Name: "严格对象",
|
||||
Constraints: map[string]any{"additionalProperties": false}},
|
||||
}
|
||||
|
||||
schema := params.ToJSONSchema()
|
||||
|
||||
open := schema.Properties["open"]
|
||||
if open.AdditionalProperties == nil {
|
||||
t.Fatal("open: expected additionalProperties to be set")
|
||||
}
|
||||
// true → Not == nil
|
||||
if open.AdditionalProperties.Not != nil {
|
||||
t.Error("open: additionalProperties should be true (Not == nil)")
|
||||
}
|
||||
|
||||
strict := schema.Properties["strict"]
|
||||
if strict.AdditionalProperties == nil {
|
||||
t.Fatal("strict: expected additionalProperties to be set")
|
||||
}
|
||||
// false → Not != nil
|
||||
if strict.AdditionalProperties.Not == nil {
|
||||
t.Error("strict: additionalProperties should be false (Not != nil)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResApiParams_AnyType_Print(t *testing.T) {
|
||||
b, _ := json.MarshalIndent(createParams.ToJSONSchema(), "", " ")
|
||||
t.Log("\n" + string(b))
|
||||
}
|
||||
|
||||
func TestResApiParams_RoundTrip(t *testing.T) {
|
||||
input := &jschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jschema.Schema{
|
||||
"name": {
|
||||
Type: "string",
|
||||
Description: "姓名",
|
||||
MinLength: jschema.Ptr(1),
|
||||
MaxLength: jschema.Ptr(100),
|
||||
},
|
||||
"age": {
|
||||
Type: "integer",
|
||||
Minimum: jschema.Ptr(float64(0)),
|
||||
Maximum: jschema.Ptr(float64(150)),
|
||||
},
|
||||
"tags": {
|
||||
Type: "array",
|
||||
Items: &jschema.Schema{Type: "string"},
|
||||
},
|
||||
},
|
||||
Required: []string{"name"},
|
||||
}
|
||||
|
||||
params := base.ResApiParamsFromJSONSchema(input)
|
||||
output := params.ToJSONSchema()
|
||||
|
||||
if _, ok := output.Properties["name"]; !ok {
|
||||
t.Error("round-trip: missing property name")
|
||||
}
|
||||
if _, ok := output.Properties["age"]; !ok {
|
||||
t.Error("round-trip: missing property age")
|
||||
}
|
||||
tags, ok := output.Properties["tags"]
|
||||
if !ok {
|
||||
t.Error("round-trip: missing property tags")
|
||||
} else if tags.Items == nil || tags.Items.Type != "string" {
|
||||
t.Errorf("round-trip: tags.items expected type=string, got %+v", tags.Items)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
{
|
||||
"tools": [
|
||||
{
|
||||
"name": "res-query",
|
||||
"description": "Query records with filters, joins, aggregates, ordering, and pagination.\rThe main resource and each hasOne/hasMany relation are independent query contexts; inner/left/right are JOINs within their parent context.\rTop-level conditions / groupBy / orderBy / limit / offset apply to the main resource SQL. Each relation has its own conditions / groupBy / orderBy / limit that apply to that relation's SQL.\rIMPORTANT:\r(1) All field codes MUST come from res-schema results already in this conversation — never guess or infer field names.\r(2) Fields belonging to a hasOne/hasMany context must use object form with codeResource set to that relation's code.\r(3) Call res-schema first if the resource schema has not been fetched yet.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conditions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/properties/conditions"
|
||||
},
|
||||
"title": "子条件",
|
||||
"description": "子条件节点,递归结构"
|
||||
},
|
||||
"exprs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"title": "字段编码",
|
||||
"description": "必须来自 res-schema 返回的字段列表,禁止猜测字段名"
|
||||
},
|
||||
"fieldResource": {
|
||||
"type": "string",
|
||||
"title": "所属资源",
|
||||
"description": "字段所属资源标识,取值为主资源编码或当前查询上下文(conditions 所在层级)对应的 relations[].code"
|
||||
},
|
||||
"fieldSqlFunc": {
|
||||
"type": "string",
|
||||
"title": "列SQL函数",
|
||||
"description": "对列应用 SQL 函数后再参与比较,拼接为 {func}({fieldResource}.{field}) {operator} {token}。\r\n支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。\r\n需要第二参数的函数(如 ROUND、CAST)配合 fieldSqlFuncParam 使用。"
|
||||
},
|
||||
"fieldSqlFuncParam": {
|
||||
"type": "string",
|
||||
"title": "列SQL函数参数",
|
||||
"description": "fieldSqlFunc 的附加参数,按函数类型用途不同:\r\r通用函数(有 param 时):{func}({fieldResource}.{field}, {fieldSqlFuncParam}) {operator} {token}\r 示例:ROUND(col, 2) → fieldSqlFuncParam=\\\"2\\\"\r SUBSTRING(col, 1, 10) → fieldSqlFuncParam=\\\"1, 10\\\""
|
||||
},
|
||||
"ignoreEmptyParam": {
|
||||
"type": "boolean",
|
||||
"title": "忽略空参数",
|
||||
"description": "当 tokenType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项",
|
||||
"default": false
|
||||
},
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"title": "运算符",
|
||||
"description": "标准 SQL 比较运算符:\r\n= / != / > / >= / < / <=\r\nLIKE:模糊匹配(%keyword%)\r\nIN:包含,value 逗号分隔多值\r\nIS NULL / IS NOT NULL:空值判断,无需 value\r\nREGEXP:正则匹配",
|
||||
"default": "="
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"title": "比较值",
|
||||
"description": "比较目标,内容由 tokenType 决定。operator 为 IS NULL/IS NOT NULL 时可省略"
|
||||
},
|
||||
"tokenType": {
|
||||
"type": "string",
|
||||
"title": "比较值类型",
|
||||
"description": "先选类型再填 token:\r\nstring:token 填字面量如 '2024-01-01'\r\nfunc:token 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparam:token 填请求参数路径如 body.status\r\nsql:token 填原始SQL如 CURRENT_DATE",
|
||||
"default": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"func"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "条件项",
|
||||
"description": "单条比较表达式,生成 [{fieldSqlFunc}(]{fieldResource}.{field}[)] {operator} {token} 片段",
|
||||
"required": [
|
||||
"field",
|
||||
"operator"
|
||||
]
|
||||
},
|
||||
"title": "条件表达式列表",
|
||||
"description": "当前层的叶子比较列表,每项生成一个 {fieldResource}.{field} {operator} {token} 片段,同层多项以 type 连接"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "逻辑连接符",
|
||||
"description": "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
|
||||
"default": "and",
|
||||
"enum": [
|
||||
"and",
|
||||
"or"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "树形查询条件",
|
||||
"description": "WHERE 条件树。\rexprs 每项生成一段比较:[{fieldSqlFunc}(]{fieldResource}.{field}[)] {operator} {token},同层多项以 type(and/or) 连接;\rchildren 为同结构子节点,递归嵌套实现复杂过滤。",
|
||||
"required": [
|
||||
"exprs",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alias": {
|
||||
"type": "string",
|
||||
"title": "别名",
|
||||
"description": "输出字段别名,对应 SQL AS。isExpr=true 时必填,否则结果列无法被引用;普通字段不填则默认使用 code 值"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"title": "字段编码",
|
||||
"description": "两种互斥用法,每个对象只能选其一:\r1. 字段编码,格式为 {Resource}.{field},Resource 取值为主资源编码或 relations[].code,如 User.amount / Order.price,isExpr 保持默认 false\r2. 原始 SQL 表达式(如 COUNT(*) / SUM(amount)),必须同时设置 isExpr = true,且 alias 必填\r注意:SELECT 中混入聚合表达式与非聚合字段时,需配合 groupBy 使用,否则结果不符合预期\r重要:字段编码必须来自 res-schema 返回的字段列表,禁止猜测或推断字段名"
|
||||
},
|
||||
"codeResource": {
|
||||
"type": "string",
|
||||
"title": "所属资源",
|
||||
"description": "字段所属的表标识,取值为主资源编码或 relations[].code"
|
||||
},
|
||||
"dataType": {
|
||||
"type": "string",
|
||||
"title": "数据类型",
|
||||
"description": "指定字段的返回数据类型,影响序列化方式;不填则由资源字段定义决定",
|
||||
"default": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"bool",
|
||||
"number",
|
||||
"array",
|
||||
"json",
|
||||
"integer",
|
||||
"float"
|
||||
]
|
||||
},
|
||||
"isExpr": {
|
||||
"type": "boolean",
|
||||
"title": "是否表达式",
|
||||
"description": "设为 true 时,code 内容作为原始 SQL 表达式拼入 SELECT(如聚合函数 COUNT(*) / SUM(amount)),而非普通字段编码。isExpr=true 时 alias 为必填项",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"title": "资源字段",
|
||||
"description": "结构化字段,支持指定所属资源、输出别名、聚合表达式及数据类型",
|
||||
"required": [
|
||||
"code",
|
||||
"codeResource"
|
||||
]
|
||||
},
|
||||
"title": "查询字段",
|
||||
"description": "SELECT 字段列表,涵盖所有查询上下文(主资源 + 所有 hasOne/hasMany),通过 codeResource 路由到对应的独立 SQL。不传则返回所有字段。\r主资源和每个 hasOne/hasMany 各是一个独立查询上下文,inner/left/right 只是其所在上下文内的 JOIN。\r仅查询聚合结果(如 COUNT(*) / SUM(amount))时,fields 只传聚合字段,不要混入普通字段,除非同时提供 groupBy。\r重要:所有字段编码必须先通过 res-schema 确认,禁止凭名称语义猜测"
|
||||
},
|
||||
"groupBy": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"title": "字段编码",
|
||||
"description": "格式必须为 {Resource}.{field},如 User.status。\r{Resource} 取值为主资源编码或 relations[].code"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expr": {
|
||||
"type": "string",
|
||||
"title": "表达式",
|
||||
"description": "type=field 时填 {Resource}.{field}(如 User.status)\rtype=sql 时填原始 SQL 表达式(如 DATE(created_at))"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "表达式类型",
|
||||
"description": "field:按资源字段分组,expr 填 {Resource}.{field},如 User.status\rsql:按原始 SQL 表达式分组,expr 填任意合法 SQL,如 DATE(created_at)",
|
||||
"default": "sql",
|
||||
"enum": [
|
||||
"field",
|
||||
"sql"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "分组表达式",
|
||||
"description": "结构化分组项,type=field 用于资源字段,type=sql 用于原始 SQL 表达式",
|
||||
"required": [
|
||||
"expr",
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "分组规则",
|
||||
"description": "GROUP BY 列表,配合 fields 中的聚合表达式使用。\r字段必须带资源前缀({Resource}.{field});聚合函数(COUNT / SUM 等)放在 fields 中,不属于此处"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"title": "每页条数",
|
||||
"description": "当前查询上下文返回的最大记录数(顶层时限制主资源结果,relations 内时限制该关联结果)",
|
||||
"default": 30
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"title": "偏移量",
|
||||
"description": "主资源查询跳过的记录数,仅顶层有效,relations 内不支持",
|
||||
"default": 0
|
||||
},
|
||||
"orderBy": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"title": "字段编码",
|
||||
"description": "格式必须为 {Resource}.{field},如 User.created_at,默认升序(ASC)。\r需要降序或使用 SQL 表达式时使用对象形式"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"title": "排序方向",
|
||||
"default": "asc",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
]
|
||||
},
|
||||
"expr": {
|
||||
"type": "string",
|
||||
"title": "表达式",
|
||||
"description": "type=field 时填 {Resource}.{field}(如 User.created_at);type=sql 时填原始 SQL 表达式(如 COUNT(*) / DATE(created_at))"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "表达式类型",
|
||||
"description": "field:按资源字段排序,expr 填 {Resource}.{field},如 User.created_at\rsql:按原始 SQL 表达式排序,expr 填任意合法 SQL,如 COUNT(*) / DATE(created_at)",
|
||||
"default": "field",
|
||||
"enum": [
|
||||
"field",
|
||||
"sql"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "排序表达式",
|
||||
"description": "结构化排序项,type=field 用于资源字段,type=sql 用于原始 SQL 表达式",
|
||||
"required": [
|
||||
"type",
|
||||
"expr"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "排序规则",
|
||||
"description": "ORDER BY 列表,多项按顺序依次应用。\r字段必须带资源前缀({Resource}.{field});需要降序或 SQL 表达式排序时使用对象形式"
|
||||
},
|
||||
"relations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"actuallyField": {
|
||||
"type": "string",
|
||||
"title": "实际资源字段",
|
||||
"description": "目标资源(code)参与 ON 条件的字段"
|
||||
},
|
||||
"actuallyResource": {
|
||||
"type": "string",
|
||||
"title": "实际资源",
|
||||
"description": "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"title": "关联标识",
|
||||
"description": "关联唯一标识,同时作为别名:JOIN 时为表别名(AS code),子查询时为结果 JSON 字段的键名。\r通常与 actuallyResource 保持一致;仅当多个关联引用同一资源导致冲突时,才需要重命名加以区分"
|
||||
},
|
||||
"conditions": {
|
||||
"$ref": "#/properties/conditions",
|
||||
"description": "该关联的过滤条件,结构与顶层 conditions 相同。hasOne/hasMany 作用于子查询 WHERE;inner/left/right 作用于 JOIN ON 或主查询 WHERE"
|
||||
},
|
||||
"groupBy": {
|
||||
"$ref": "#/properties/groupBy",
|
||||
"description": "该关联的 GROUP BY,仅 hasOne/hasMany 子查询生效,结构与顶层 groupBy 相同"
|
||||
},
|
||||
"limit": {
|
||||
"$ref": "#/properties/limit",
|
||||
"description": "该关联的最大返回条数,仅 hasOne/hasMany 子查询生效,结构与顶层 limit 相同"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "关联名称",
|
||||
"description": "关联的显示名称"
|
||||
},
|
||||
"orderBy": {
|
||||
"$ref": "#/properties/orderBy",
|
||||
"description": "该关联的 ORDER BY,仅 hasOne/hasMany 子查询生效,结构与顶层 orderBy 相同"
|
||||
},
|
||||
"relationField": {
|
||||
"type": "string",
|
||||
"title": "被关联资源字段",
|
||||
"description": "ON 条件对端资源的字段"
|
||||
},
|
||||
"relationResource": {
|
||||
"type": "string",
|
||||
"title": "被关联资源",
|
||||
"description": "ON 条件对端的资源编码(通常为主资源或父关联资源)"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "关联类型",
|
||||
"description": "关联模式:\r\ninner / left / right:SQL JOIN,要求与主资源同一数据库连接,将关联字段合并到主查询行\r\nhasOne:独立子查询,支持跨库,结果聚合为单个对象(result[code] = {})\r\nhasMany:独立子查询,支持跨库,结果聚合为对象数组(result[code] = [...])\r\n跨数据库连接的关联必须使用 hasOne 或 hasMany",
|
||||
"enum": [
|
||||
"inner",
|
||||
"left",
|
||||
"right",
|
||||
"hasOne",
|
||||
"hasMany"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actuallyField",
|
||||
"actuallyResource",
|
||||
"code",
|
||||
"relationField",
|
||||
"relationResource",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"title": "资源关联",
|
||||
"description": "关联配置列表。\rinner / left / right:SQL JOIN,要求关联资源与主资源在同一数据库连接,将关联字段合并到主查询行。\rhasOne / hasMany:独立子查询,支持跨数据库连接,结果聚合为 JSON 字段(hasOne 为单对象,hasMany 为数组)。\r跨库关联必须使用 hasOne 或 hasMany,不可使用 JOIN 类型。"
|
||||
},
|
||||
"resource": {
|
||||
"type": "string",
|
||||
"title": "主资源",
|
||||
"description": "主资源编码(resource_code),作为本次查询的根上下文,relations 中的关联资源均以此为基础展开"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fields",
|
||||
"resource"
|
||||
]
|
||||
},
|
||||
"annotations": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
{
|
||||
"tools": [
|
||||
{
|
||||
"name": "res-query",
|
||||
"description": "Query records with filters, joins, aggregates, ordering, pagination, and recursive tree traversal.\rAll relations are SQL JOINs within a single query; the main resource is the primary table.\rPrefer JOIN over multiple queries: when the needed data spans several resources, combine same-connection resources into one query using relations, then make separate queries only for resources on different connections.\rWRONG: 3 queries — User → UserRole → Role, even though User and UserRole share the same connection.\rRIGHT: 1 query joining User + UserRole (same connection), then 1 separate query for Role (different connection).\rIMPORTANT:\r(1) Never guess field names — only use codes from res-schema results in this conversation.\r(2) Call res-schema first if the resource schema has not been fetched yet.\r(3) Use recursive to traverse all descendants of hierarchical data (e.g. categories, menus, org charts); returns a flat list, not a nested tree.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conditions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/properties/conditions"
|
||||
},
|
||||
"title": "子条件",
|
||||
"description": "子条件节点,递归结构"
|
||||
},
|
||||
"exprs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"title": "字段编码",
|
||||
"description": "必须来自 res-schema 返回的字段列表,禁止猜测字段名"
|
||||
},
|
||||
"fieldResource": {
|
||||
"type": "string",
|
||||
"title": "所属资源",
|
||||
"description": "字段所属的表标识,取值为主资源编码或 relations[].code"
|
||||
},
|
||||
"fieldSqlFunc": {
|
||||
"type": "string",
|
||||
"title": "列SQL函数",
|
||||
"description": "对列应用 SQL 函数后再参与比较,拼接为 {func}({fieldResource}.{field}) {operator} {token}。\r\n支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。\r\n需要第二参数的函数(如 ROUND、CAST)配合 fieldSqlFuncParam 使用。"
|
||||
},
|
||||
"fieldSqlFuncParam": {
|
||||
"type": "string",
|
||||
"title": "列SQL函数参数",
|
||||
"description": "fieldSqlFunc 的附加参数,按函数类型用途不同:\r\r通用函数(有 param 时):{func}({fieldResource}.{field}, {fieldSqlFuncParam}) {operator} {token}\r 示例:ROUND(col, 2) → fieldSqlFuncParam=\"2\"\r SUBSTRING(col, 1, 10) → fieldSqlFuncParam=\"1, 10\"\r\rJSON 函数(param 为 JSON 路径表达式):\r json_member_of(token 是否是列 JSON 数组的成员):\r 无 param:JSON_CONTAINS({token}, JSON_ARRAY({field}))\r 有 param:JSON_CONTAINS({fieldSqlFuncParam}, JSON_ARRAY({field}))\r json_contains(列 JSON 是否包含 token):\r 无 param:JSON_CONTAINS({field}, JSON_ARRAY({token}))\r 有 param:JSON_CONTAINS({fieldSqlFuncParam}, JSON_ARRAY({token}))"
|
||||
},
|
||||
"ignoreEmptyParam": {
|
||||
"type": "boolean",
|
||||
"title": "忽略空参数",
|
||||
"description": "当 tokenType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项",
|
||||
"default": false
|
||||
},
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"title": "运算符",
|
||||
"description": "标准 SQL 比较运算符:\r\n= / != / > / >= / < / <=\r\nLIKE:模糊匹配(%keyword%)\r\nIN:包含,token 逗号分隔多值\r\nIS NULL / IS NOT NULL:空值判断,无需 token\r\nREGEXP:正则匹配",
|
||||
"default": "="
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"title": "比较值",
|
||||
"description": "比较目标,内容由 tokenType 决定。operator 为 IS NULL/IS NOT NULL 时可省略"
|
||||
},
|
||||
"tokenType": {
|
||||
"type": "string",
|
||||
"title": "比较值类型",
|
||||
"description": "先选类型再填 token:\r\nstring:token 填字面量如 '2024-01-01'\r\nfunc:token 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparam:token 填请求参数路径如 body.status\r\nsql:token 填原始SQL如 CURRENT_DATE",
|
||||
"default": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"func"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "条件项",
|
||||
"description": "单条比较表达式,生成 [{fieldSqlFunc}(]{fieldResource}.{field}[)] {operator} {token} 片段",
|
||||
"required": [
|
||||
"field",
|
||||
"operator"
|
||||
]
|
||||
},
|
||||
"title": "条件表达式列表",
|
||||
"description": "当前层的叶子比较列表,每项生成一个 {fieldResource}.{field} {operator} {token} 片段,同层多项以 type 连接"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "逻辑连接符",
|
||||
"description": "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
|
||||
"default": "and",
|
||||
"enum": [
|
||||
"and",
|
||||
"or"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "树形查询条件",
|
||||
"description": "WHERE 条件树。\rexprs 每项生成一段比较:[{fieldSqlFunc}(]{fieldResource}.{field}[)] {operator} {token},同层多项以 type(and/or) 连接;\rchildren 为同结构子节点,递归嵌套实现复杂过滤。",
|
||||
"required": [
|
||||
"exprs",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alias": {
|
||||
"type": "string",
|
||||
"title": "别名",
|
||||
"description": "输出字段别名,对应 SQL AS。isExpr=true 时必填;普通字段在多表 JOIN 存在同名冲突时填写,其余情况默认使用 code 值"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"title": "字段编码",
|
||||
"description": "两种互斥用法,每个对象只能选其一:\r1. 普通字段编码,只填字段名本身如 amount,不含资源前缀,所属表由 codeResource 单独指定,isExpr 保持默认 false\r2. 原始 SQL 表达式(如 COUNT(*) / SUM(User.amount)),可含 {Resource}.{field} 引用,必须同时设置 isExpr = true 且 alias 必填\r注意:SELECT 中混入聚合表达式与非聚合字段时,需配合 groupBy 使用,否则结果不符合预期\r重要:字段编码必须来自 res-schema 返回的字段列表,禁止猜测或推断字段名"
|
||||
},
|
||||
"codeResource": {
|
||||
"type": "string",
|
||||
"title": "所属资源",
|
||||
"description": "字段所属的表标识,取值为主资源编码或 relations[].code"
|
||||
},
|
||||
"dataType": {
|
||||
"type": "string",
|
||||
"title": "数据类型",
|
||||
"description": "指定字段的返回数据类型,影响序列化方式;不填则由资源字段定义决定",
|
||||
"default": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"bool",
|
||||
"number",
|
||||
"array",
|
||||
"json",
|
||||
"integer",
|
||||
"float"
|
||||
]
|
||||
},
|
||||
"isExpr": {
|
||||
"type": "boolean",
|
||||
"title": "是否表达式",
|
||||
"description": "设为 true 时,code 内容作为原始 SQL 表达式拼入 SELECT(如聚合函数 COUNT(*) / SUM(amount)),而非普通字段编码。isExpr=true 时 alias 为必填项",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"title": "资源字段",
|
||||
"description": "结构化字段,支持指定所属表、输出别名、聚合表达式及数据类型",
|
||||
"required": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
"title": "查询字段",
|
||||
"description": "SELECT 字段列表,不传则返回所有字段(仅主资源字段,不含 JOIN 表)。\r使用 relations 时,JOIN 表的字段不会自动出现在结果中,必须在 fields 里显式列出需要的 JOIN 表字段,否则 JOIN 无意义。\r普通字段:code 填字段名,codeResource 指定所属表;聚合/表达式:isExpr=true,code 填 SQL 表达式,alias 必填。\r仅查询聚合结果时,fields 只传聚合字段,不要混入普通字段,除非同时提供 groupBy。\r重要:所有字段编码必须先通过 res-schema 确认,禁止凭名称语义猜测"
|
||||
},
|
||||
"groupBy": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"title": "字段编码",
|
||||
"description": "格式必须为 {Resource}.{field},如 User.status。\r{Resource} 取值为主资源编码或 relations[].code"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expr": {
|
||||
"type": "string",
|
||||
"title": "表达式",
|
||||
"description": "type=field 时填 {Resource}.{field}(如 User.status)\rtype=sql 时填原始 SQL 表达式(如 DATE(created_at))"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "表达式类型",
|
||||
"description": "field:按资源字段分组,expr 填 {Resource}.{field},如 User.status\rsql:按原始 SQL 表达式分组,expr 填任意合法 SQL,如 DATE(created_at)",
|
||||
"default": "field",
|
||||
"enum": [
|
||||
"field",
|
||||
"sql"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "分组表达式",
|
||||
"description": "结构化分组项,type=field 用于资源字段,type=sql 用于原始 SQL 表达式",
|
||||
"required": [
|
||||
"type",
|
||||
"expr"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "分组规则",
|
||||
"description": "GROUP BY 列表,配合 fields 中的聚合表达式使用。\r字段必须带资源前缀({Resource}.{field});聚合函数(COUNT / SUM 等)放在 fields 中,不属于此处"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"title": "每页条数",
|
||||
"description": "返回的最大记录数",
|
||||
"default": 30
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"title": "偏移量",
|
||||
"description": "跳过的记录数",
|
||||
"default": 0
|
||||
},
|
||||
"orderBy": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"title": "字段编码",
|
||||
"description": "格式必须为 {Resource}.{field},如 User.created_at,默认升序(ASC)。\r需要降序或使用 SQL 表达式时使用对象形式"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"title": "排序方向",
|
||||
"default": "asc",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
]
|
||||
},
|
||||
"expr": {
|
||||
"type": "string",
|
||||
"title": "表达式",
|
||||
"description": "type=field 时填 {Resource}.{field}(如 User.created_at);type=sql 时填原始 SQL 表达式(如 COUNT(*) / DATE(created_at))"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "表达式类型",
|
||||
"description": "field:按资源字段排序,expr 填 {Resource}.{field},如 User.created_at\rsql:按原始 SQL 表达式排序,expr 填任意合法 SQL,如 COUNT(*) / DATE(created_at)",
|
||||
"default": "field",
|
||||
"enum": [
|
||||
"field",
|
||||
"sql"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "排序表达式",
|
||||
"description": "结构化排序项,type=field 用于资源字段,type=sql 用于原始 SQL 表达式",
|
||||
"required": [
|
||||
"type",
|
||||
"expr"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "排序规则",
|
||||
"description": "ORDER BY 列表,多项按顺序依次应用。\r字段必须带资源前缀({Resource}.{field});需要降序或 SQL 表达式排序时使用对象形式"
|
||||
},
|
||||
"relations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"actuallyField": {
|
||||
"type": "string",
|
||||
"title": "实际资源字段",
|
||||
"description": "目标资源(code)参与 ON 条件的字段"
|
||||
},
|
||||
"actuallyResource": {
|
||||
"type": "string",
|
||||
"title": "实际资源",
|
||||
"description": "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"title": "关联标识",
|
||||
"description": "JOIN 表别名(AS code),同时作为 fields/conditions/groupBy/orderBy 中 {Resource} 的取值。\r通常与 actuallyResource 保持一致;仅当多个关联引用同一资源导致冲突时,才需要重命名"
|
||||
},
|
||||
"conditions": {
|
||||
"$ref": "#/properties/conditions",
|
||||
"description": "该 JOIN 的附加过滤条件,作用于 JOIN ON 或主查询 WHERE,结构与顶层 conditions 相同"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "关联名称",
|
||||
"description": "关联的显示名称"
|
||||
},
|
||||
"relationField": {
|
||||
"type": "string",
|
||||
"title": "被关联资源字段",
|
||||
"description": "ON 条件对端资源的字段"
|
||||
},
|
||||
"relationResource": {
|
||||
"type": "string",
|
||||
"title": "被关联资源",
|
||||
"description": "ON 条件对端的资源编码(通常为主资源或父关联资源)"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "关联类型",
|
||||
"description": "SQL JOIN 类型(仅支持同库连接,不可跨库):\rinner:内连接,只返回两表均匹配的行\rleft:左连接,保留主表所有行\rright:右连接,保留关联表所有行",
|
||||
"enum": [
|
||||
"inner",
|
||||
"left",
|
||||
"right"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actuallyField",
|
||||
"actuallyResource",
|
||||
"code",
|
||||
"relationField",
|
||||
"relationResource",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"title": "资源关联",
|
||||
"description": "SQL JOIN 列表,所有关联在同一条 SQL 中执行。\r关联资源的字段通过 code(表别名)在 fields / conditions / groupBy / orderBy 中引用。\r重要:所有关联资源必须与主资源在同一数据库连接。res-schema 返回的资源名称后面标有连接标识(如 · default / · service-support),建立 JOIN 前必须确认主资源与所有关联资源的连接标识完全一致,不一致则不可使用 JOIN,需分次查询后在应用层合并。"
|
||||
},
|
||||
"resource": {
|
||||
"type": "string",
|
||||
"title": "主资源",
|
||||
"description": "主资源编码(resource),作为本次查询的主表,relations 中的关联资源均以此为基础 JOIN"
|
||||
},
|
||||
"recursive": {
|
||||
"type": "object",
|
||||
"title": "递归查询",
|
||||
"description": "树形/层级数据的递归查询配置。配置后会以 pField=root 的记录为根节点,向下递归遍历所有子孙节点,返回平铺的记录列表(非嵌套树)。",
|
||||
"properties": {
|
||||
"pField": {
|
||||
"type": "string",
|
||||
"title": "父节点字段",
|
||||
"description": "存储父节点 ID 的字段编码,格式为 {Resource}.{field}。{Resource} 取值为主资源编码或 relations[].code,如 Category.pid"
|
||||
},
|
||||
"cField": {
|
||||
"type": "string",
|
||||
"title": "当前节点字段",
|
||||
"description": "存储自身 ID 的字段编码,格式为 {Resource}.{field}。{Resource} 取值为主资源编码或 relations[].code,如 Category.id"
|
||||
},
|
||||
"root": {
|
||||
"type": "string",
|
||||
"title": "根节点值",
|
||||
"description": "递归起点,pField 等于此值的记录作为根节点,通常为 0 或空字符串"
|
||||
},
|
||||
"depth": {
|
||||
"type": "integer",
|
||||
"title": "递归深度",
|
||||
"description": "最大递归层数,防止循环引用导致无限递归,不传则不限制"
|
||||
}
|
||||
},
|
||||
"required": ["pField", "cField", "root"]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fields",
|
||||
"resource"
|
||||
]
|
||||
},
|
||||
"annotations": {}
|
||||
},
|
||||
{
|
||||
"name": "res-create",
|
||||
"description": "Create one or multiple new records for the specified resource. Returns all fields of the created record(s).\rCall res-schema first to get the field list before building the data object.\rIMPORTANT: Never guess field names — only use codes from res-schema results.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"type": "string",
|
||||
"title": "主资源",
|
||||
"description": "目标资源编码,数据将写入该资源对应的表"
|
||||
},
|
||||
"data": {
|
||||
"title": "创建数据",
|
||||
"description": "单条记录传对象,批量创建传对象数组。\rkey 为字段编码(必须来自 res-schema 返回的字段列表,禁止猜测),value 为写入值。\r有默认值的字段可以不传,系统会自动填充。\r批量创建(array)时,所有记录必须包含完全相同的字段集合,不可部分记录有某字段而其他记录没有。\r系统保留字段禁止传入,包括:id、created_at、updated_at、created_user、owned_user、enabled 及其他由系统自动维护的字段",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"title": "单条记录",
|
||||
"description": "创建单条记录,key 为字段编码,value 为字段值",
|
||||
"additionalProperties": true
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"title": "批量记录",
|
||||
"description": "批量创建,每个元素为一条记录对象",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["resource", "data"]
|
||||
},
|
||||
"annotations": {}
|
||||
},
|
||||
{
|
||||
"name": "res-update",
|
||||
"description": "Update records by their IDs. Returns only the updated fields of the modified records.\rBefore calling this tool, ALWAYS use res-query to retrieve and show the user the records that will be affected. Only call res-update after the user has reviewed and confirmed.\rPass __confirm__: true when the user has approved the update.\rIMPORTANT: Never guess field names — only use codes from res-schema results.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"type": "string",
|
||||
"title": "主资源",
|
||||
"description": "目标资源编码"
|
||||
},
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"title": "记录ID列表",
|
||||
"description": "必填,指定要更新的记录 ID 列表。必须通过 res-query 预先查询获得,禁止手动构造。",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"title": "更新数据",
|
||||
"description": "只传需要修改的字段,未传字段保持不变。\rkey 为字段编码(必须来自 res-schema 返回的字段列表,禁止猜测),value 为新值。\r系统保留字段禁止传入,包括:id、created_at、updated_at、created_user、owned_user、enabled 及其他由系统自动维护的字段",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"__confirm__": {
|
||||
"type": "boolean",
|
||||
"title": "确认执行",
|
||||
"description": "首次调用不传。仅当服务端返回需要确认的提示时,重新调用并传入 true 以执行更新。不可主动传入,否则将跳过安全确认步骤。",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": ["resource", "ids", "data"]
|
||||
},
|
||||
"annotations": {}
|
||||
},
|
||||
{
|
||||
"name": "res-delete",
|
||||
"description": "Hard-delete records by ID (physical removal, unrecoverable).\rWARNING: Prefer soft delete — use res-update to set deleted_at=CURRENT_TIMESTAMP and enabled=0 instead.\rOnly call res-delete when the user explicitly requests permanent/physical deletion.\rBefore calling, ALWAYS use res-query to show the user the affected records and obtain confirmation.\rPass __confirm__: true after the user has confirmed.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"type": "string",
|
||||
"title": "主资源",
|
||||
"description": "目标资源编码"
|
||||
},
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"title": "记录ID列表",
|
||||
"description": "必填,指定要删除的记录 ID 列表。必须通过 res-query 预先查询获得,禁止手动构造。",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"__confirm__": {
|
||||
"type": "boolean",
|
||||
"title": "确认执行",
|
||||
"description": "首次调用不传。仅当服务端返回需要确认的提示时,重新调用并传入 true 以执行删除。不可主动传入,否则将跳过安全确认步骤。",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": ["resource", "ids"]
|
||||
},
|
||||
"annotations": {}
|
||||
},
|
||||
{
|
||||
"name": "res-schema",
|
||||
"description": "Look up available resources and their field definitions. Without keywords: lists all resources. With keywords: returns matched resources with their field list, types, and constraints. Keywords match against resource code, name, or table name; supports Chinese fuzzy matching; use | to separate multiple terms (e.g. \"用户|权限\"). Call this before using res-query / res-update / res-delete if the resource schema is not already in the current conversation.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"keywords": {
|
||||
"type": "string",
|
||||
"title": "查找关键字",
|
||||
"description": "搜索关键词,用于筛选资源列表。支持多个关键词,用 | 分隔。\r对每条资源的编码(code)、名称(name)、数据表名(table)进行匹配,\r支持中文分词模糊匹配,结果按匹配相关度从高到低排序。\r示例:\"用户\" 或 \"用户|权限\""
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,758 @@
|
||||
[
|
||||
{
|
||||
"id": 271,
|
||||
"pid": 0,
|
||||
"code": "fields",
|
||||
"name": "查询字段",
|
||||
"category": "param",
|
||||
"type": "array",
|
||||
"desc": "查询字段,不传则返回所有字段",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 282,
|
||||
"pid": 0,
|
||||
"code": "groupBy",
|
||||
"name": "分组规则",
|
||||
"category": "param",
|
||||
"type": "array",
|
||||
"desc": "GROUP BY 字段列表,配合 fields 中的聚合字段使用",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 287,
|
||||
"pid": 0,
|
||||
"code": "orderBy",
|
||||
"name": "排序规则",
|
||||
"category": "param",
|
||||
"type": "array",
|
||||
"desc": "排序规则列表,多项按顺序依次应用",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 299,
|
||||
"pid": 0,
|
||||
"code": "relations",
|
||||
"name": "资源关联",
|
||||
"category": "param",
|
||||
"type": "array",
|
||||
"desc": "关联配置列表,支持 JOIN(inner / left / right)将关联字段合并到主查询,或子查询(hasOne / hasMany)将关联数据聚合为独立 JSON 字段",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 258,
|
||||
"pid": 0,
|
||||
"code": "conditions",
|
||||
"name": "树形查询条件",
|
||||
"category": "param",
|
||||
"type": "json",
|
||||
"desc": "WHERE 条件树。\rexprs 每项生成一段比较:[{columnSqlFunc}(]{columnResource}.{column}[)] {operator} {value},同层多项以 type(and/or) 连接;\rchildren 为同结构子节点,递归嵌套实现复杂过滤。",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 297,
|
||||
"pid": 0,
|
||||
"code": "limit",
|
||||
"name": "每页条数",
|
||||
"category": "param",
|
||||
"type": "integer",
|
||||
"desc": "返回的最大记录数",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": "30"
|
||||
},
|
||||
{
|
||||
"id": 298,
|
||||
"pid": 0,
|
||||
"code": "offset",
|
||||
"name": "偏移量",
|
||||
"category": "param",
|
||||
"type": "integer",
|
||||
"desc": "跳过的记录数",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": "0"
|
||||
},
|
||||
{
|
||||
"id": 259,
|
||||
"pid": 258,
|
||||
"code": "type",
|
||||
"name": "逻辑连接符",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
|
||||
"isRequired": true,
|
||||
"constraints": {
|
||||
"enum": [
|
||||
"and",
|
||||
"or"
|
||||
]
|
||||
},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": "and"
|
||||
},
|
||||
{
|
||||
"id": 261,
|
||||
"pid": 258,
|
||||
"code": "children",
|
||||
"name": "子条件",
|
||||
"category": "param",
|
||||
"type": "array",
|
||||
"desc": "子条件节点,递归结构",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 260,
|
||||
"pid": 258,
|
||||
"code": "exprs",
|
||||
"name": "条件表达式列表",
|
||||
"category": "param",
|
||||
"type": "array",
|
||||
"desc": "当前层的叶子比较列表,每项生成一个 column {operator} value 片段,同层多项以 type 连接",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 262,
|
||||
"pid": 260,
|
||||
"code": "items",
|
||||
"name": "_items",
|
||||
"category": "param",
|
||||
"type": "json",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 312,
|
||||
"pid": 261,
|
||||
"code": "items",
|
||||
"name": "_items",
|
||||
"category": "param",
|
||||
"type": "json",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 258,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 263,
|
||||
"pid": 262,
|
||||
"code": "column",
|
||||
"name": "字段编码",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 264,
|
||||
"pid": 262,
|
||||
"code": "columnResource",
|
||||
"name": "所属资源",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 268,
|
||||
"pid": 262,
|
||||
"code": "columnSqlFunc",
|
||||
"name": "列SQL函数",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "对列应用 SQL 函数后再参与比较,拼接为 {func}({columnResource}.{column}) {operator} {value}。\r\n支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。\r\n需要第二参数的函数(如 ROUND、CAST)配合 columnSqlFuncParam 使用。",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 269,
|
||||
"pid": 262,
|
||||
"code": "columnSqlFuncParam",
|
||||
"name": "列SQL函数参数",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "columnSqlFunc 的附加参数,按函数类型用途不同:\r\r通用函数(有 param 时):{func}({columnResource}.{column}, {columnSqlFuncParam}) {operator} {value}\r 示例:ROUND(col, 2) → columnSqlFuncParam=\"2\"\r SUBSTRING(col, 1, 10) → columnSqlFuncParam=\"1, 10\"\r\rJSON 函数(param 为 JSON 路径表达式):\r json_member_of(value 是否是列 JSON 数组的成员):\r 无 param:JSON_CONTAINS({value}, JSON_ARRAY({column}))\r 有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({column}))\r json_contains(列 JSON 是否包含 value):\r 无 param:JSON_CONTAINS({column}, JSON_ARRAY({value}))\r 有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({value}))",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 265,
|
||||
"pid": 262,
|
||||
"code": "operator",
|
||||
"name": "运算符",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "标准 SQL 比较运算符:\r\n= / != / \u003e / \u003e= / \u003c / \u003c=\r\nLIKE:模糊匹配(%keyword%)\r\nIN:包含,value 逗号分隔多值\r\nIS NULL / IS NOT NULL:空值判断,无需 value\r\nREGEXP:正则匹配",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": "="
|
||||
},
|
||||
{
|
||||
"id": 266,
|
||||
"pid": 262,
|
||||
"code": "value",
|
||||
"name": "比较值",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "比较目标,内容由 valueType 决定。operator 为 IS NULL/IS NOT NULL 时可省略",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 267,
|
||||
"pid": 262,
|
||||
"code": "valueType",
|
||||
"name": "比较值类型",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "先选类型再填 value:\r\nstring:value 填字面量如 '2024-01-01'\r\nfunc:value 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparam:value 填请求参数路径如 body.status\r\nsql:value 填原始SQL如 CURRENT_DATE",
|
||||
"isRequired": false,
|
||||
"constraints": {
|
||||
"enum": [
|
||||
"string",
|
||||
"func"
|
||||
]
|
||||
},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": "string"
|
||||
},
|
||||
{
|
||||
"id": 270,
|
||||
"pid": 262,
|
||||
"code": "ignoreEmptyParam",
|
||||
"name": "忽略空参数",
|
||||
"category": "param",
|
||||
"type": "bool",
|
||||
"desc": "当 valueType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": "false"
|
||||
},
|
||||
{
|
||||
"id": 272,
|
||||
"pid": 271,
|
||||
"code": "oneOf",
|
||||
"name": "字段编码",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "字段引用,格式:{columnResource}.{column} 或 {column}",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 279,
|
||||
"pid": 271,
|
||||
"code": "oneOf",
|
||||
"name": "表达式字段",
|
||||
"category": "param",
|
||||
"type": "json",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 275,
|
||||
"pid": 271,
|
||||
"code": "oneOf",
|
||||
"name": "资源字段",
|
||||
"category": "param",
|
||||
"type": "json",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 278,
|
||||
"pid": 275,
|
||||
"code": "alias",
|
||||
"name": "别名",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 276,
|
||||
"pid": 275,
|
||||
"code": "column",
|
||||
"name": "字段编码",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 277,
|
||||
"pid": 275,
|
||||
"code": "columnResource",
|
||||
"name": "所属资源",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 281,
|
||||
"pid": 279,
|
||||
"code": "alias",
|
||||
"name": "别名",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 280,
|
||||
"pid": 279,
|
||||
"code": "expr",
|
||||
"name": "SQL 表达式",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 283,
|
||||
"pid": 282,
|
||||
"code": "oneOf",
|
||||
"name": "分组表达式",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 284,
|
||||
"pid": 282,
|
||||
"code": "oneOf",
|
||||
"name": "资源字段",
|
||||
"category": "param",
|
||||
"type": "json",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 285,
|
||||
"pid": 284,
|
||||
"code": "column",
|
||||
"name": "字段编码",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 286,
|
||||
"pid": 284,
|
||||
"code": "columnResource",
|
||||
"name": "所属资源",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "字段所属资源标识,取值为顶层 resource 或 relations[].code",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 288,
|
||||
"pid": 287,
|
||||
"code": "oneOf",
|
||||
"name": "SQL 表达式 (ASC)",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用\r默认升序(asc)",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 289,
|
||||
"pid": 287,
|
||||
"code": "oneOf",
|
||||
"name": "SQL 表达式排序",
|
||||
"category": "param",
|
||||
"type": "json",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 292,
|
||||
"pid": 287,
|
||||
"code": "oneOf",
|
||||
"name": "资源字段排序",
|
||||
"category": "param",
|
||||
"type": "json",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 291,
|
||||
"pid": 289,
|
||||
"code": "direction",
|
||||
"name": "排序方向",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
]
|
||||
},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": "asc"
|
||||
},
|
||||
{
|
||||
"id": 290,
|
||||
"pid": 289,
|
||||
"code": "expr",
|
||||
"name": "SQL 表达式",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 293,
|
||||
"pid": 292,
|
||||
"code": "column",
|
||||
"name": "字段编码",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 295,
|
||||
"pid": 292,
|
||||
"code": "columnResource",
|
||||
"name": "所属资源",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 296,
|
||||
"pid": 292,
|
||||
"code": "direction",
|
||||
"name": "排序方向",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
]
|
||||
},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": "asc"
|
||||
},
|
||||
{
|
||||
"id": 300,
|
||||
"pid": 299,
|
||||
"code": "items",
|
||||
"name": "",
|
||||
"category": "param",
|
||||
"type": "json",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 305,
|
||||
"pid": 300,
|
||||
"code": "actuallyField",
|
||||
"name": "实际资源字段",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "目标资源(code)参与 ON 条件的字段",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 304,
|
||||
"pid": 300,
|
||||
"code": "actuallyResource",
|
||||
"name": "实际资源",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 303,
|
||||
"pid": 300,
|
||||
"code": "code",
|
||||
"name": "关联标识",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "关联唯一标识,同时作为别名:JOIN 时为表别名(AS code),子查询时为结果 JSON 字段的键名",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 302,
|
||||
"pid": 300,
|
||||
"code": "name",
|
||||
"name": "关联名称",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "关联的显示名称",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 307,
|
||||
"pid": 300,
|
||||
"code": "relationField",
|
||||
"name": "被关联资源字段",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "ON 条件对端资源的字段",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 306,
|
||||
"pid": 300,
|
||||
"code": "relationResource",
|
||||
"name": "被关联资源",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "ON 条件对端的资源编码(通常为主资源或父关联资源)",
|
||||
"isRequired": true,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 301,
|
||||
"pid": 300,
|
||||
"code": "type",
|
||||
"name": "关联类型",
|
||||
"category": "param",
|
||||
"type": "string",
|
||||
"desc": "关联模式:\r\ninner / left / right:SQL JOIN,将关联资源字段合并到主查询行\r\nhasOne:子查询,关联结果聚合为单个对象(result[code] = {})\r\nhasMany:子查询,关联结果聚合为对象数组(result[code] = [...])",
|
||||
"isRequired": true,
|
||||
"constraints": {
|
||||
"enum": [
|
||||
"inner",
|
||||
"left",
|
||||
"right",
|
||||
"hasOne",
|
||||
"hasMany"
|
||||
]
|
||||
},
|
||||
"rank": 0,
|
||||
"ref_id": 0,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 309,
|
||||
"pid": 300,
|
||||
"code": "groupBy",
|
||||
"name": "关联分组,仅 hasOne/hasMany 有效",
|
||||
"category": "param",
|
||||
"type": "array",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 282,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 310,
|
||||
"pid": 300,
|
||||
"code": "orderBy",
|
||||
"name": "关联排序,仅 hasOne/hasMany 有效",
|
||||
"category": "param",
|
||||
"type": "array",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 287,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 308,
|
||||
"pid": 300,
|
||||
"code": "conditions",
|
||||
"name": "关联的过滤条件",
|
||||
"category": "param",
|
||||
"type": "json",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 258,
|
||||
"df_val": ""
|
||||
},
|
||||
{
|
||||
"id": 311,
|
||||
"pid": 300,
|
||||
"code": "limit",
|
||||
"name": "关联条数限制,仅 hasOne/hasMany 有效",
|
||||
"category": "param",
|
||||
"type": "integer",
|
||||
"desc": "",
|
||||
"isRequired": false,
|
||||
"constraints": {},
|
||||
"rank": 0,
|
||||
"ref_id": 297,
|
||||
"df_val": ""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,324 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conditions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/properties/conditions"
|
||||
},
|
||||
"title": "子条件",
|
||||
"description": "子条件节点,递归结构"
|
||||
},
|
||||
"exprs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"column": {
|
||||
"type": "string",
|
||||
"title": "字段编码"
|
||||
},
|
||||
"columnResource": {
|
||||
"type": "string",
|
||||
"title": "所属资源",
|
||||
"description": "字段所属资源标识,取值为顶层 resource 或 relations[].code"
|
||||
},
|
||||
"columnSqlFunc": {
|
||||
"type": "string",
|
||||
"title": "列SQL函数",
|
||||
"description": "对列应用 SQL 函数后再参与比较,拼接为 {func}({columnResource}.{column}) {operator} {value}。\r\n支持 SQL-92 / SQL:1999 / SQL:2003 标准函数及数据库方言函数,如 DATE、UPPER、ROUND。\r\n需要第二参数的函数(如 ROUND、CAST)配合 columnSqlFuncParam 使用。"
|
||||
},
|
||||
"columnSqlFuncParam": {
|
||||
"type": "string",
|
||||
"title": "列SQL函数参数",
|
||||
"description": "columnSqlFunc 的附加参数,按函数类型用途不同:\r\r通用函数(有 param 时):{func}({columnResource}.{column}, {columnSqlFuncParam}) {operator} {value}\r 示例:ROUND(col, 2) → columnSqlFuncParam=\"2\"\r SUBSTRING(col, 1, 10) → columnSqlFuncParam=\"1, 10\"\r\rJSON 函数(param 为 JSON 路径表达式):\r json_member_of(value 是否是列 JSON 数组的成员):\r 无 param:JSON_CONTAINS({value}, JSON_ARRAY({column}))\r 有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({column}))\r json_contains(列 JSON 是否包含 value):\r 无 param:JSON_CONTAINS({column}, JSON_ARRAY({value}))\r 有 param:JSON_CONTAINS({columnSqlFuncParam}, JSON_ARRAY({value}))"
|
||||
},
|
||||
"ignoreEmptyParam": {
|
||||
"type": "boolean",
|
||||
"title": "忽略空参数",
|
||||
"description": "当 valueType=param 且请求参数值为空(空字符串或 null)时,跳过该条件不生成 SQL。用于实现可选过滤项",
|
||||
"default": false
|
||||
},
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"title": "运算符",
|
||||
"description": "标准 SQL 比较运算符:\r\n= / != / \u003e / \u003e= / \u003c / \u003c=\r\nLIKE:模糊匹配(%keyword%)\r\nIN:包含,value 逗号分隔多值\r\nIS NULL / IS NOT NULL:空值判断,无需 value\r\nREGEXP:正则匹配",
|
||||
"default": "="
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"title": "比较值",
|
||||
"description": "比较目标,内容由 valueType 决定。operator 为 IS NULL/IS NOT NULL 时可省略"
|
||||
},
|
||||
"valueType": {
|
||||
"type": "string",
|
||||
"title": "比较值类型",
|
||||
"description": "先选类型再填 value:\r\nstring:value 填字面量如 '2024-01-01'\r\nfunc:value 填内置函数名 UserID|UserUuid|UserRolesUuid|UserPlatform|UserSaaS\r\nparam:value 填请求参数路径如 body.status\r\nsql:value 填原始SQL如 CURRENT_DATE",
|
||||
"default": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"func"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "_items",
|
||||
"description": "_items",
|
||||
"required": [
|
||||
"column",
|
||||
"operator"
|
||||
]
|
||||
},
|
||||
"title": "条件表达式列表",
|
||||
"description": "当前层的叶子比较列表,每项生成一个 column {operator} value 片段,同层多项以 type 连接"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "逻辑连接符",
|
||||
"description": "当前节点的逻辑运算符,将 exprs 各项与 children 各组以 AND 或 OR 连接",
|
||||
"default": "and",
|
||||
"enum": [
|
||||
"and",
|
||||
"or"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "树形查询条件",
|
||||
"description": "WHERE 条件树。\rexprs 每项生成一段比较:[{columnSqlFunc}(]{columnResource}.{column}[)] {operator} {value},同层多项以 type(and/or) 连接;\rchildren 为同结构子节点,递归嵌套实现复杂过滤。",
|
||||
"required": [
|
||||
"exprs",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"title": "字段编码",
|
||||
"description": "字段引用,格式:{columnResource}.{column} 或 {column}"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alias": {
|
||||
"type": "string",
|
||||
"title": "别名"
|
||||
},
|
||||
"expr": {
|
||||
"type": "string",
|
||||
"title": "SQL 表达式",
|
||||
"description": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用"
|
||||
}
|
||||
},
|
||||
"title": "表达式字段"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alias": {
|
||||
"type": "string",
|
||||
"title": "别名"
|
||||
},
|
||||
"column": {
|
||||
"type": "string",
|
||||
"title": "字段编码"
|
||||
},
|
||||
"columnResource": {
|
||||
"type": "string",
|
||||
"title": "所属资源",
|
||||
"description": "字段所属资源标识,取值为顶层 resource 或 relations[].code"
|
||||
}
|
||||
},
|
||||
"title": "资源字段",
|
||||
"required": [
|
||||
"column"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "查询字段",
|
||||
"description": "查询字段,不传则返回所有字段"
|
||||
},
|
||||
"groupBy": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"title": "分组表达式",
|
||||
"description": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"column": {
|
||||
"type": "string",
|
||||
"title": "字段编码"
|
||||
},
|
||||
"columnResource": {
|
||||
"type": "string",
|
||||
"title": "所属资源",
|
||||
"description": "字段所属资源标识,取值为顶层 resource 或 relations[].code"
|
||||
}
|
||||
},
|
||||
"title": "资源字段",
|
||||
"required": [
|
||||
"column"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "分组规则",
|
||||
"description": "GROUP BY 字段列表,配合 fields 中的聚合字段使用"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"title": "每页条数",
|
||||
"description": "返回的最大记录数",
|
||||
"default": 30
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"title": "偏移量",
|
||||
"description": "跳过的记录数",
|
||||
"default": 0
|
||||
},
|
||||
"orderBy": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"title": "SQL 表达式 (ASC)",
|
||||
"description": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用\r默认升序(asc)"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"title": "排序方向",
|
||||
"default": "asc",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
]
|
||||
},
|
||||
"expr": {
|
||||
"type": "string",
|
||||
"title": "SQL 表达式",
|
||||
"description": "SQL 表达式:如 COUNT(*) / SUM(amount)\r{column}:字段编码,默认归属顶层 resource\r{columnResource}.{column}:显式指定资源的字段引用"
|
||||
}
|
||||
},
|
||||
"title": "SQL 表达式排序"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"column": {
|
||||
"type": "string",
|
||||
"title": "字段编码"
|
||||
},
|
||||
"columnResource": {
|
||||
"type": "string",
|
||||
"title": "所属资源"
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"title": "排序方向",
|
||||
"default": "asc",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "资源字段排序",
|
||||
"required": [
|
||||
"column",
|
||||
"columnResource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "排序规则",
|
||||
"description": "排序规则列表,多项按顺序依次应用"
|
||||
},
|
||||
"relations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"actuallyField": {
|
||||
"type": "string",
|
||||
"title": "实际资源字段",
|
||||
"description": "目标资源(code)参与 ON 条件的字段"
|
||||
},
|
||||
"actuallyResource": {
|
||||
"type": "string",
|
||||
"title": "实际资源",
|
||||
"description": "被关联的目标资源编码(相当于 SQL 表名)。JOIN 拼接为:{type} JOIN {actuallyResource} AS {code} ON {code}.{actuallyField} = {relationResource}.{relationField}"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"title": "关联标识",
|
||||
"description": "关联唯一标识,同时作为别名:JOIN 时为表别名(AS code),子查询时为结果 JSON 字段的键名"
|
||||
},
|
||||
"conditions": {
|
||||
"$ref": "#/properties/conditions"
|
||||
},
|
||||
"groupBy": {
|
||||
"$ref": "#/properties/groupBy"
|
||||
},
|
||||
"limit": {
|
||||
"$ref": "#/properties/limit"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "关联名称",
|
||||
"description": "关联的显示名称"
|
||||
},
|
||||
"orderBy": {
|
||||
"$ref": "#/properties/orderBy"
|
||||
},
|
||||
"relationField": {
|
||||
"type": "string",
|
||||
"title": "被关联资源字段",
|
||||
"description": "ON 条件对端资源的字段"
|
||||
},
|
||||
"relationResource": {
|
||||
"type": "string",
|
||||
"title": "被关联资源",
|
||||
"description": "ON 条件对端的资源编码(通常为主资源或父关联资源)"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "关联类型",
|
||||
"description": "关联模式:\r\ninner / left / right:SQL JOIN,将关联资源字段合并到主查询行\r\nhasOne:子查询,关联结果聚合为单个对象(result[code] = {})\r\nhasMany:子查询,关联结果聚合为对象数组(result[code] = [...])",
|
||||
"enum": [
|
||||
"inner",
|
||||
"left",
|
||||
"right",
|
||||
"hasOne",
|
||||
"hasMany"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actuallyField",
|
||||
"actuallyResource",
|
||||
"code",
|
||||
"relationField",
|
||||
"relationResource",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"title": "资源关联",
|
||||
"description": "关联配置列表,支持 JOIN(inner / left / right)将关联字段合并到主查询,或子查询(hasOne / hasMany)将关联数据聚合为独立 JSON 字段"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package base
|
||||
|
||||
import "git.fsdpf.net/go/condition"
|
||||
import (
|
||||
"git.fsdpf.net/go/condition"
|
||||
)
|
||||
|
||||
type ResCondition struct {
|
||||
Id int `db:"id"`
|
||||
@@ -53,7 +55,7 @@ func NewConditionByRes(items []ResCondition, opts ...condition.Option) (root *co
|
||||
conditions[item.Id] = condition.NewExpr(
|
||||
item.ColumnResource, item.Column,
|
||||
condition.Token(item.Value, condition.TokenType(item.ValueType)),
|
||||
condition.Operator(condition.ConditionOperator(item.Operator)),
|
||||
condition.Operator(condition.ConditionOperator(condition.GetDBOperator(item.Operator))),
|
||||
condition.FieldSqlFn(item.ColumnSqlFunc, item.ColumnSqlFuncParam),
|
||||
condition.IgnoreEmptyParma(item.IgnoreEmptyParma),
|
||||
)
|
||||
|
||||
+61
-11
@@ -1,16 +1,66 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.fsdpf.net/go/reflux"
|
||||
"git.fsdpf.net/go/reflux/valuex"
|
||||
)
|
||||
|
||||
// role_uuid => value
|
||||
type ResRoleValue map[string]string
|
||||
|
||||
type ResConfigure struct {
|
||||
Uuid string `db:"uuid"`
|
||||
ResourceUuid string `db:"resource_uuid"`
|
||||
Key string `db:"key"`
|
||||
Value string `db:"value"`
|
||||
Label string `db:"label"`
|
||||
Type string `db:"type"`
|
||||
IsPrivate bool `db:"isPrivate"`
|
||||
Desc string `db:"desc"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
Uuid string `db:"uuid"`
|
||||
ResourceUuid string `db:"resource_uuid"`
|
||||
Key string `db:"key"`
|
||||
Value string `db:"value"`
|
||||
Label string `db:"label"`
|
||||
Type string `db:"type"`
|
||||
IsPrivate bool `db:"isPrivate"`
|
||||
Desc string `db:"desc"`
|
||||
RoleValues []ResRoleValue `db:"role_values"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
type GetResConfigures func(uuid string) []ResConfigure
|
||||
// GetValueByRoles 根据角色优先级获取配置值
|
||||
//
|
||||
// - roles: 角色UUIDs
|
||||
//
|
||||
// 返回: valuex.Accessor 对象和是否找到角色特定值的标志
|
||||
func (rc *ResConfigure) GetValueByRoles(roles ...string) (valuex.Accessor, bool) {
|
||||
// 如果没有角色值配置或没有传入角色,返回默认值
|
||||
if len(rc.RoleValues) == 0 || len(roles) == 0 {
|
||||
return reflux.New(rc.toTyped(rc.Value)), true
|
||||
}
|
||||
|
||||
for _, rv := range rc.RoleValues {
|
||||
for _, role := range roles {
|
||||
if value, exists := rv[role]; exists {
|
||||
return reflux.New(rc.toTyped(value)), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 没有找到匹配的角色值,返回默认值
|
||||
return reflux.New(rc.toTyped(rc.Value)), false
|
||||
}
|
||||
|
||||
func (rc *ResConfigure) toTyped(s string) any {
|
||||
switch rc.Type {
|
||||
case "json":
|
||||
var v map[string]any
|
||||
if json.Unmarshal([]byte(s), &v) == nil {
|
||||
return v
|
||||
}
|
||||
case "array":
|
||||
var v []any
|
||||
if json.Unmarshal([]byte(s), &v) == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type GetResConfigure func(key string, roles ...string) (valuex.Accessor, bool)
|
||||
|
||||
+4
-261
@@ -1,266 +1,9 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.fsdpf.net/go/contracts/res_type"
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/db/schema"
|
||||
"git.fsdpf.net/go/req"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cast"
|
||||
"git.fsdpf.net/go/req/resx"
|
||||
)
|
||||
|
||||
// 资源字段
|
||||
type ResField struct {
|
||||
Uuid string `db:"uuid"`
|
||||
Name string `db:"name"`
|
||||
Code string `db:"code"`
|
||||
CodeResource string `db:"codeResource"`
|
||||
DataType req.ResDataType `db:"table_type"`
|
||||
Length string `db:"length"`
|
||||
Comment string `db:"comment"`
|
||||
Default string `db:"default"`
|
||||
}
|
||||
|
||||
func (this ResField) ToStructField(tags ...string) reflect.StructField {
|
||||
var typ reflect.Type
|
||||
|
||||
fCode := this.Code
|
||||
fTag := `db:"` + fCode + `" json:"` + fCode + `"`
|
||||
|
||||
if len(tags) > 0 {
|
||||
fTag = strings.Join(tags, " ") + " " + fTag
|
||||
}
|
||||
|
||||
switch this.DataType {
|
||||
case req.ResString, req.ResText, req.ResEnum,
|
||||
req.ResTimestamp, req.ResDate, req.ResDatetime:
|
||||
typ = reflect.TypeOf(string(""))
|
||||
case req.ResInteger, req.ResSmallInteger:
|
||||
typ = reflect.TypeOf(res_type.ResFieldByInteger(0))
|
||||
case req.ResDecimal:
|
||||
typ = reflect.TypeOf(float64(0))
|
||||
case req.ResBoolean:
|
||||
typ = reflect.TypeOf(true)
|
||||
case req.ResJson:
|
||||
if this.Default != "" && this.Default[0:1] == "[" {
|
||||
typ = reflect.TypeOf([]any{})
|
||||
} else {
|
||||
typ = reflect.TypeOf(map[string]any{})
|
||||
}
|
||||
}
|
||||
|
||||
return reflect.StructField{
|
||||
Name: strings.ToUpper(fCode[:1]) + fCode[1:],
|
||||
Tag: reflect.StructTag(fTag),
|
||||
Type: typ,
|
||||
}
|
||||
}
|
||||
|
||||
func (this ResField) GetCode() string {
|
||||
return this.Code
|
||||
}
|
||||
|
||||
func (this ResField) GetCodeResource() string {
|
||||
return this.CodeResource
|
||||
}
|
||||
|
||||
func (this ResField) GetName() string {
|
||||
return this.Name
|
||||
}
|
||||
|
||||
func (this ResField) GetDataType() req.ResDataType {
|
||||
return this.DataType
|
||||
}
|
||||
|
||||
func (this ResField) GetQueryDataType() req.RouteParamType {
|
||||
switch this.GetDataType() {
|
||||
case req.ResEnum,
|
||||
req.ResTimestamp, req.ResDate,
|
||||
req.ResDatetime, req.ResString,
|
||||
req.ResText:
|
||||
return req.ReqString
|
||||
case req.ResInteger, req.ResSmallInteger:
|
||||
return req.ReqInteger
|
||||
case req.ResDecimal:
|
||||
return req.ReqFloat
|
||||
case req.ResBoolean:
|
||||
return req.ReqBool
|
||||
case req.ResJson:
|
||||
if this.Default != "" && this.Default[0:1] == "[" {
|
||||
return req.ReqArray
|
||||
}
|
||||
return req.ReqJson
|
||||
}
|
||||
return req.ReqString
|
||||
}
|
||||
|
||||
func (this ResField) ToValue(v any) any {
|
||||
switch this.DataType {
|
||||
case req.ResString, req.ResText, req.ResEnum,
|
||||
req.ResTimestamp, req.ResDate, req.ResDatetime:
|
||||
return strings.Trim(cast.ToString(v), " ")
|
||||
case req.ResInteger, req.ResSmallInteger:
|
||||
return cast.ToInt(v)
|
||||
case req.ResDecimal:
|
||||
return strings.Trim(cast.ToString(v), " ")
|
||||
case req.ResBoolean:
|
||||
if v, _ := strconv.ParseBool(fmt.Sprintf("%v", v)); v {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case req.ResJson:
|
||||
if v == nil {
|
||||
if this.Default != "" && this.Default[0:1] == "[" {
|
||||
return db.Raw("'[]'")
|
||||
} else if this.Default != "" && this.Default[0:1] == "{" {
|
||||
return db.Raw("'{}'")
|
||||
} else if this.Default == "" {
|
||||
return db.Raw("'{}'")
|
||||
}
|
||||
|
||||
return this.Default
|
||||
}
|
||||
|
||||
if str, ok := v.(string); ok {
|
||||
return str
|
||||
} else if raw, ok := v.(db.Expression); ok {
|
||||
return raw
|
||||
} else if b, err := json.Marshal(v); err == nil {
|
||||
return string(b)
|
||||
} else {
|
||||
panic(fmt.Sprintf("%s, 类型转换错误, %s", this.Code, err))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Trim(cast.ToString(v), " ")
|
||||
}
|
||||
|
||||
func (this ResField) GetRawDefault(driver string) db.Expression {
|
||||
if this.DataType == req.ResJson {
|
||||
if this.Default != "" && this.Default[0:1] == "[" {
|
||||
return db.Raw("'[]'")
|
||||
} else if this.Default != "" && this.Default[0:1] == "{" {
|
||||
return db.Raw("'{}'")
|
||||
} else if this.Default == "" {
|
||||
return db.Raw("'{}'")
|
||||
}
|
||||
} else if this.DataType == req.ResBoolean {
|
||||
if v, _ := strconv.ParseBool(this.Default); v {
|
||||
return db.Raw("'1'")
|
||||
}
|
||||
return db.Raw("'0'")
|
||||
}
|
||||
|
||||
if len(this.Default) > 4 && strings.ToLower(this.Default[0:4]) == "sql:" {
|
||||
sql := strings.ToLower(this.Default[4:])
|
||||
if sql == "uuid()" {
|
||||
if driver == "sqlite" {
|
||||
return db.Raw("'" + uuid.NewString() + "'")
|
||||
}
|
||||
return db.Raw("uuid()")
|
||||
}
|
||||
return db.Raw(sql)
|
||||
}
|
||||
|
||||
if this.Default == "" {
|
||||
if this.GetDataType() == req.ResDate || this.GetDataType() == req.ResDatetime {
|
||||
return db.Raw("NULL")
|
||||
}
|
||||
return db.Raw("''")
|
||||
}
|
||||
|
||||
if strings.ToUpper(this.Default) == "CURRENT_TIMESTAMP" {
|
||||
return db.Raw(this.Default)
|
||||
}
|
||||
|
||||
return db.Raw("'" + this.Default + "'")
|
||||
}
|
||||
|
||||
func (this ResField) ToBlueprint(table *schema.Blueprint) (temp *schema.ColumnDefinition) {
|
||||
isNull := false
|
||||
comment := this.Name
|
||||
def := any(this.Default)
|
||||
|
||||
if this.Comment != "" {
|
||||
comment += " [ " + strings.Trim(this.Comment, `' "`) + " ]"
|
||||
}
|
||||
|
||||
switch this.DataType {
|
||||
case "string":
|
||||
len := 255
|
||||
if v, err := strconv.Atoi(strings.Trim(this.Length, `' "`)); err == nil {
|
||||
len = v
|
||||
}
|
||||
temp = table.String(this.Code, len)
|
||||
case "smallInteger":
|
||||
// integer 默认长度 4
|
||||
temp = table.SmallInteger(this.Code)
|
||||
case "boolean":
|
||||
// integer 默认长度 1
|
||||
temp = table.Boolean(this.Code)
|
||||
case "integer":
|
||||
// integer 默认长度 11
|
||||
temp = table.Integer(this.Code)
|
||||
case "date", "dateTime":
|
||||
if this.DataType == "date" {
|
||||
temp = table.Date(this.Code)
|
||||
} else {
|
||||
temp = table.DateTime(this.Code)
|
||||
}
|
||||
|
||||
if strings.ToUpper(this.Default) == "CURRENT_TIMESTAMP" {
|
||||
def = db.Raw(this.Default)
|
||||
} else if def == "" {
|
||||
isNull = true
|
||||
}
|
||||
case "decimal":
|
||||
allowed := strings.SplitN(this.Length, ",", 2)
|
||||
total := 8
|
||||
places := 2
|
||||
if v, err := strconv.Atoi(strings.Trim(allowed[0], `' "`)); err == nil {
|
||||
total = v
|
||||
}
|
||||
if v, err := strconv.Atoi(strings.Trim(allowed[1], `' "`)); err == nil {
|
||||
places = v
|
||||
}
|
||||
temp = table.Decimal(this.Code, total, places)
|
||||
case "enum":
|
||||
allowed := []string{}
|
||||
for _, v := range strings.Split(this.Length, ",") {
|
||||
allowed = append(allowed, strings.Trim(v, `' "`))
|
||||
}
|
||||
temp = table.Enum(this.Code, allowed)
|
||||
case "json":
|
||||
temp = table.Json(this.Code)
|
||||
isNull = true
|
||||
case "text":
|
||||
temp = table.Text(this.Code)
|
||||
isNull = true
|
||||
}
|
||||
|
||||
if isNull {
|
||||
temp.Nullable()
|
||||
} else {
|
||||
temp.Default(def)
|
||||
}
|
||||
|
||||
temp.Comment(comment)
|
||||
|
||||
return temp
|
||||
}
|
||||
|
||||
func (this ResField) ToQueryField(t req.RouteParamType, alias string, options byte) req.QueryField {
|
||||
o := &QueryField{
|
||||
ResField: this,
|
||||
typ: t,
|
||||
alias: alias,
|
||||
}
|
||||
|
||||
return o.SetOptions(options)
|
||||
}
|
||||
// ResField 的实现搬到了 git.fsdpf.net/go/req/resx(resx 自己需要用它做字段级脱敏,
|
||||
// 不能反过来 import contracts/base,见 resx.ResField 的注释),这里保留类型别名兼容原有引用。
|
||||
type ResField = resx.ResField
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/db/schema"
|
||||
"git.fsdpf.net/go/req"
|
||||
"git.fsdpf.net/go/req/resx"
|
||||
|
||||
_ "git.fsdpf.net/go/db/schema/dialect/mysql"
|
||||
_ "git.fsdpf.net/go/db/schema/dialect/sqlite3"
|
||||
)
|
||||
|
||||
func TestResFieldToBlueprint(t *testing.T) {
|
||||
t.Run("id", func(t *testing.T) {
|
||||
table := schema.NewBlueprint("users")
|
||||
|
||||
id := resx.NewResField("id", "User", resx.FieldWithUuid("00000000-0000-0000-0000-000000000000"), resx.FieldWithName("ID"), resx.FieldWithLength("20"), resx.FieldWithDefault("sql:AUTO_INCREMENT"), resx.FieldWithDataType(req.ResInteger))
|
||||
|
||||
id.ToBlueprint(table)
|
||||
})
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ResFields []ResField
|
||||
|
||||
func (c *ResFields) Scan(value any) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(value.([]byte), c); err != nil {
|
||||
return fmt.Errorf("ResFields json.Unmarshal Error, %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ResFields) Value() (driver.Value, error) {
|
||||
b, err := json.Marshal(c)
|
||||
return string(b), err
|
||||
}
|
||||
+36
-2
@@ -1,14 +1,48 @@
|
||||
package base
|
||||
|
||||
import "time"
|
||||
|
||||
type ResJob struct {
|
||||
Uuid string `db:"uuid"`
|
||||
Code string `db:"code"`
|
||||
Name string `db:"name"`
|
||||
ResourceUuid string `db:"resource_uuid"`
|
||||
Replay int64 `db:"replay"`
|
||||
IsRecord bool `db:"isRecord"`
|
||||
MaxAttempts int `db:"maxAttempts"` // 最大重试次数
|
||||
Concurrent int `db:"concurrent"` // 同类 job 最大并发数,0 表示不限制
|
||||
Timeout int `db:"timeout"` // 单次执行超时秒数,0 表示不限制
|
||||
Retry int `db:"retry"` // 重试间隔秒数,0 表示不重试,>0 表示固定间隔秒数
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
type GetResJob func(code string) (ResJob, bool)
|
||||
|
||||
const (
|
||||
FailedJobStatusResolved = 0 // 已解决(成功完成)
|
||||
FailedJobStatusPending = 1 // 待重试
|
||||
FailedJobStatusExceeded = 2 // 已超出最大重试次数
|
||||
FailedJobStatusInterrupted = 3 // shutdown 中断
|
||||
)
|
||||
|
||||
type ResFailedJob struct {
|
||||
ID int64 `db:"id"`
|
||||
Queue string `db:"queue"` // 投递队列(ResJob.Code)
|
||||
Payload string `db:"payload"` // JSON
|
||||
Platform string `db:"platform"`
|
||||
Saas string `db:"saas"`
|
||||
TraceId string `db:"trace_id"`
|
||||
Exception string `db:"exception"`
|
||||
Attempts int `db:"attempts"`
|
||||
MaxAttempts int `db:"maxAttempts"`
|
||||
Status int `db:"status"`
|
||||
RetryAt *time.Time `db:"retry_at"`
|
||||
RetriedAt *time.Time `db:"retried_at"`
|
||||
OwnedUser string `db:"owned_user"` // 触发任务的用户 uuid
|
||||
CreatedAt string `db:"created_at"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
}
|
||||
|
||||
// CanRetry 是否还可以重试
|
||||
func (j ResFailedJob) CanRetry() bool {
|
||||
return j.Status == FailedJobStatusPending && j.Attempts < j.MaxAttempts
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package base
|
||||
|
||||
type ResListener struct {
|
||||
Uuid string `db:"uuid"`
|
||||
Code string `db:"code"`
|
||||
Name string `db:"name"`
|
||||
ResourceUuid string `db:"resource_uuid"`
|
||||
Event []any `db:"event"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
type GetResListens func(categoryUuid string, event ...string) []ResListener
|
||||
@@ -0,0 +1,20 @@
|
||||
package base
|
||||
|
||||
// ResWatcher 描述一条"资源写操作监听"注册记录:Code 对应一个实现了 contracts.ResWatcher 的处理器,
|
||||
// ResourceUuids 是它关心的资源(支持 1-n 个)
|
||||
type ResWatcher struct {
|
||||
Uuid string `db:"uuid"`
|
||||
Code string `db:"code"`
|
||||
Name string `db:"name"`
|
||||
ResourceUuids []string `db:"resource_uuid"`
|
||||
OwnedUser string `db:"owned_user"` // Handle 执行时使用的用户身份(同 Cron/Mqtt 约定)
|
||||
MaxDepth int `db:"max_depth"` // 允许级联触发的最大深度,用于防止 watcher 之间循环触发
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
// GetResWatcher 按 code 查询单条 ResWatcher 注册记录
|
||||
type GetResWatcher func(code string) (ResWatcher, bool)
|
||||
|
||||
// GetResWatchers 按资源 uuid 反查关心该资源的 ResWatcher 注册记录
|
||||
type GetResWatchers func(resourceUuid string) []ResWatcher
|
||||
@@ -1,458 +0,0 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"unicode"
|
||||
|
||||
"github.com/samber/do"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"git.fsdpf.net/go/condition"
|
||||
"git.fsdpf.net/go/contracts"
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
// 资源变更事件
|
||||
const ResChangeEventTopic = "res-change-event-topic"
|
||||
|
||||
// 资源变更数据
|
||||
const ResChangeRecordTopic = "res-change-record-topic"
|
||||
|
||||
type ResChangeEventTopicPayload struct {
|
||||
Type string // insert | delete | update
|
||||
Res Resource // 变更资源
|
||||
Result sql.Result // 执行结果
|
||||
}
|
||||
|
||||
type ResChangeRecordTopicPayload struct {
|
||||
Type string // insert | delete | update
|
||||
User req.User // 操作用户
|
||||
Res Resource // 变更资源
|
||||
Result sql.Result // 执行结果
|
||||
Old []map[string]any // 旧数据
|
||||
New []map[string]any // 新数据
|
||||
}
|
||||
|
||||
// 资源
|
||||
type Resource struct {
|
||||
container *do.Injector
|
||||
|
||||
Uuid string `db:"uuid"`
|
||||
PUuid string `db:"pUuid"`
|
||||
Code string `db:"code"`
|
||||
Name string `db:"name"`
|
||||
IsResVirtual bool `db:"isVirtual"`
|
||||
Table string `db:"table"`
|
||||
Namespace string `db:"namespace"`
|
||||
Workspace string `db:"workspace"`
|
||||
Primarykey string `db:"primaryKey"`
|
||||
IsHistoryRecord bool `db:"isHistoryRecord"`
|
||||
HistoryCacheMax int `db:"historyCacheMax"`
|
||||
Fields ResFields `db:"fields"`
|
||||
Roles map[string]any `db:"roles"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
func (this *Resource) InitContainer(container *do.Injector) {
|
||||
this.container = container
|
||||
}
|
||||
|
||||
// 资源UUID
|
||||
func (this Resource) GetUuid() string {
|
||||
return this.Uuid
|
||||
}
|
||||
|
||||
// 资源CODE
|
||||
func (this Resource) GetCode() string {
|
||||
return this.Code
|
||||
}
|
||||
|
||||
// 资源名
|
||||
func (this Resource) GetName() string {
|
||||
return this.Name
|
||||
}
|
||||
|
||||
// 主键
|
||||
func (this Resource) GetPrimarykey() string {
|
||||
return this.Primarykey
|
||||
}
|
||||
|
||||
// 是否虚拟资源
|
||||
func (this Resource) IsVirtual() bool {
|
||||
return this.IsResVirtual
|
||||
}
|
||||
|
||||
// 是否系统资源
|
||||
func (this Resource) IsSystem() bool {
|
||||
return this.Namespace == "Framework\\Service"
|
||||
}
|
||||
|
||||
// 资源字段
|
||||
func (this Resource) GetFields() (result []req.ResField) {
|
||||
for _, item := range this.Fields {
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 资源字段
|
||||
func (this Resource) GetField(code string) (req.ResField, bool) {
|
||||
return lo.Find(this.GetFields(), func(v req.ResField) bool {
|
||||
return v.GetCode() == code
|
||||
})
|
||||
}
|
||||
|
||||
// 判断资源字段
|
||||
func (this Resource) HasField(code string) bool {
|
||||
return lo.SomeBy(this.GetFields(), func(v req.ResField) bool {
|
||||
return v.GetCode() == code
|
||||
})
|
||||
}
|
||||
|
||||
// 开启事物
|
||||
func (this Resource) BeginTransaction() (*db.Transaction, error) {
|
||||
return this.GetDBConn().BeginTransaction()
|
||||
}
|
||||
|
||||
// 获取资源链接
|
||||
func (this Resource) GetDBConn() *db.Connection {
|
||||
db := do.MustInvoke[db.DB](this.container)
|
||||
|
||||
if this.IsSystem() {
|
||||
return db.Connection("service-support")
|
||||
}
|
||||
|
||||
return db.Connection("default")
|
||||
}
|
||||
|
||||
// 获取资源对应的数据库连接
|
||||
func (this Resource) GetDBBuilder() *db.Builder {
|
||||
return this.GetDBConn().Query()
|
||||
}
|
||||
|
||||
// 获取资源对应的数据表
|
||||
func (this Resource) GetTable() db.Expression {
|
||||
if this.IsVirtual() {
|
||||
return db.Raw("(" + this.Table + ")")
|
||||
}
|
||||
|
||||
return db.Raw(this.Table)
|
||||
}
|
||||
|
||||
func (this Resource) GetDBDriver() string {
|
||||
return this.GetDBConn().GetConfig().Driver
|
||||
}
|
||||
|
||||
func (this Resource) GetAuthDBTable(u req.User, params ...any) *db.Builder {
|
||||
builder := this.GetDBTable(append(params, u)...)
|
||||
|
||||
// 数据权限过滤
|
||||
builder.Before(func(b *db.Builder, t string, data ...map[string]any) {
|
||||
if t == db.TYPE_SELECT || t == db.TYPE_UPDATE || t == db.TYPE_DELETE {
|
||||
this.WithRolesCondition(b, t, u)
|
||||
}
|
||||
})
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
// GetDBTable("Test", contracts.User)
|
||||
func (this Resource) GetDBTable(params ...any) *db.Builder {
|
||||
builder := this.GetDBBuilder()
|
||||
|
||||
var user req.User
|
||||
alias := this.Code
|
||||
|
||||
for _, param := range params {
|
||||
switch v := param.(type) {
|
||||
case *db.Transaction:
|
||||
builder.Tx = v
|
||||
case string:
|
||||
alias = v
|
||||
case req.User:
|
||||
user = v
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化数据库存储数据
|
||||
builder.Before(func(b *db.Builder, t string, data ...map[string]any) {
|
||||
if t == db.TYPE_UPDATE {
|
||||
// 格式化保存数据
|
||||
this.formatSaveValue(data[0])
|
||||
}
|
||||
if t == db.TYPE_INSERT {
|
||||
// 移除 table alias
|
||||
b.Table(string(this.GetTable()))
|
||||
for i := 0; i < len(data); i++ {
|
||||
// 格式化保存数据
|
||||
this.formatSaveValue(data[i])
|
||||
// 填充保存数据
|
||||
this.fillSaveValue(data[i], user, db.TYPE_INSERT)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 资源事件
|
||||
this.onResEvent(builder)
|
||||
|
||||
// 用户事件
|
||||
if this.IsHistoryRecord {
|
||||
this.onUserEvent(builder, user)
|
||||
}
|
||||
|
||||
// 虚拟资源暂时不考虑鉴权
|
||||
if !this.IsVirtual() {
|
||||
// 返回鉴权后的 DB Builder
|
||||
// return
|
||||
}
|
||||
|
||||
return builder.Table(string(this.GetTable()), alias)
|
||||
}
|
||||
|
||||
func (this Resource) WithRolesCondition(b *db.Builder, t string, u req.User) error {
|
||||
isFullRight := false
|
||||
isFullNot := false
|
||||
|
||||
NewOrm := do.MustInvoke[contracts.NewOrm](this.container)
|
||||
NewOrmModel := do.MustInvoke[contracts.NewOrmModel](this.container)
|
||||
NewOrmJoin := do.MustInvoke[contracts.NewOrmJoin](this.container)
|
||||
|
||||
GetResRelationResource := do.MustInvoke[GetResRelationResource](this.container)
|
||||
GetResRelations := do.MustInvoke[GetResRelations](this.container)
|
||||
GetResource := do.MustInvoke[contracts.GetResource](this.container)
|
||||
GetOrmConditions := do.MustInvoke[contracts.GetOrmConditions](this.container)
|
||||
|
||||
items := do.MustInvoke[GetResRoles](this.container)(this.GetUuid(), u.Roles()...)
|
||||
|
||||
subTables := lo.Reduce(items, func(carry string, item ResRole, _ int) string {
|
||||
db := this.GetDBBuilder().Table(string(this.GetTable()), this.GetCode()).Select(db.Raw("distinct `" + this.GetCode() + "`.*"))
|
||||
|
||||
joins := lo.Filter(GetResRelations(item.Uuid), func(item ResRelation, _ int) bool {
|
||||
return item.Type == "inner" || item.Type == "left" || item.Type == "right"
|
||||
})
|
||||
|
||||
for i := 0; i < len(joins); i++ {
|
||||
oResource, ok := GetResource(joins[i].ResourceCode)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rResource, ok := GetResRelationResource(joins[i])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
join := NewOrmJoin(contracts.RelationType(joins[i].Type), oResource, joins[i].Code, joins[i].RelationResource, joins[i].RelationField, joins[i].RelationForeignKey)
|
||||
// 关联扩展条件
|
||||
join.SetCondition(GetOrmConditions(joins[i].Uuid, condition.Describe("关联扩展条件")))
|
||||
|
||||
join.Inject(db, NewOrmModel(rResource, rResource.GetCode(), rResource.GetName()))
|
||||
}
|
||||
|
||||
conditions := GetOrmConditions(item.Uuid, condition.Describe("关联扩展条件"))
|
||||
|
||||
if len(joins) == 0 && conditions.IsEmpty() {
|
||||
// 无权限, 直接跳过这个 unoin 语句
|
||||
if carry != "" {
|
||||
return carry
|
||||
}
|
||||
// 第一个无权限除外, 避免所有用户所属角色都是无权限
|
||||
db.WhereRaw("false")
|
||||
isFullNot = true
|
||||
} else if len(joins) == 0 && conditions.IsNotEmpty() && conditions.IsAlwaysRight() /* 1=1 的这种条件*/ {
|
||||
// 只要有1个满权限, 直接返回单条语句
|
||||
isFullRight = true
|
||||
return db.ToSql()
|
||||
} else if conditions.IsNotEmpty() {
|
||||
oOrm := NewOrm(this, nil)
|
||||
oOrm.SetGlobalParams(req.NewGlobalParam("{}", u))
|
||||
|
||||
db.Where(conditions.ToSql(oOrm.GetModel()))
|
||||
|
||||
// 如果前面是无权限的sql查看, 这直接返回本次查询
|
||||
if isFullNot {
|
||||
isFullNot = false
|
||||
return db.ToSql()
|
||||
}
|
||||
}
|
||||
|
||||
if carry != "" {
|
||||
carry += " UNION "
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s(%s)", carry, db.ToSql())
|
||||
}, "")
|
||||
|
||||
if isFullRight {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isFullNot {
|
||||
b.WhereRaw("false")
|
||||
} else if subTables != "" {
|
||||
if t == db.TYPE_SELECT {
|
||||
b.FromSub(subTables, b.TableAlias)
|
||||
} else {
|
||||
b.WhereRaw(fmt.Sprintf(
|
||||
"`%s`.`id` in (SELECT `temp`.`id` FROM (%s) as `temp`)",
|
||||
lo.Ternary(b.TableAlias != "", b.TableAlias, this.GetCode()),
|
||||
subTables,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 格式化保存数据
|
||||
func (this Resource) formatSaveValue(data map[string]any) {
|
||||
//
|
||||
for k, v := range data {
|
||||
if k == "id" || k == "created_user" || k == "created_at" || k == "deleted_at" || k == "updated_at" {
|
||||
delete(data, k)
|
||||
} else if val, ok := v.(db.Expression); ok {
|
||||
data[k] = val
|
||||
} else if field, ok := this.GetField(k); ok {
|
||||
data[k] = field.ToValue(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 填充保存数据
|
||||
func (this Resource) fillSaveValue(data map[string]any, u req.User, t string) {
|
||||
for _, field := range this.GetFields() {
|
||||
fCode := field.GetCode()
|
||||
|
||||
if fCode == "id" || fCode == "created_user" || fCode == "created_at" || fCode == "deleted_at" || fCode == "updated_at" || fCode == "owned_user" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 只有新增默认字段
|
||||
if _, ok := data[fCode]; !ok {
|
||||
data[fCode] = field.GetRawDefault(this.GetDBDriver())
|
||||
}
|
||||
}
|
||||
|
||||
// 拥有者
|
||||
if _, ok := data["owned_user"]; !ok {
|
||||
data["owned_user"] = u.Uuid()
|
||||
}
|
||||
// 创建者
|
||||
data["created_user"] = u.Uuid()
|
||||
if this.GetDBDriver() == "sqlite" {
|
||||
// 更新时间
|
||||
// sqlite 不能自动更新时间, "DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
|
||||
data["updated_at"] = db.Raw("CURRENT_TIMESTAMP")
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (this Resource) GetStruct(extends ...reflect.StructField) any {
|
||||
fields := []reflect.StructField{}
|
||||
|
||||
for _, field := range this.Fields {
|
||||
if unicode.IsLetter(rune(field.Code[0])) {
|
||||
fields = append(fields, field.ToStructField())
|
||||
} else {
|
||||
log.Printf("资源字段错误, 必须以字母开头 <- %s", field.Code)
|
||||
}
|
||||
}
|
||||
|
||||
fields = lo.UniqBy(append(fields, extends...), func(v reflect.StructField) string {
|
||||
return v.Name
|
||||
})
|
||||
|
||||
t := reflect.StructOf(fields)
|
||||
|
||||
return reflect.New(t).Interface()
|
||||
}
|
||||
|
||||
func (this Resource) GetSliceStruct(extends ...reflect.StructField) any {
|
||||
t := reflect.TypeOf(this.GetStruct(extends...))
|
||||
|
||||
st := reflect.SliceOf(t.Elem())
|
||||
|
||||
return reflect.New(st).Interface()
|
||||
}
|
||||
|
||||
// 资源事件
|
||||
func (this Resource) onResEvent(builder *db.Builder) {
|
||||
builder.After(func(b *db.Builder, t string, result sql.Result, err error, data ...map[string]any) {
|
||||
if err != nil || t == db.TYPE_SELECT {
|
||||
return
|
||||
} else if num, err := result.RowsAffected(); num == 0 || err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 全局触发器
|
||||
// 1. 清除系统缓存
|
||||
if err := do.MustInvoke[contracts.Queue](this.container).Publish(ResChangeEventTopic, ResChangeEventTopicPayload{
|
||||
Type: t,
|
||||
Res: this,
|
||||
Result: result,
|
||||
}); err != nil {
|
||||
log.Println("Queue Publish Err:", ResChangeEventTopic, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 用户事件
|
||||
func (this Resource) onUserEvent(builder *db.Builder, user req.User) {
|
||||
old := []map[string]any{}
|
||||
|
||||
builder.Before(func(b *db.Builder, t string, data ...map[string]any) {
|
||||
if t != db.TYPE_UPDATE && t != db.TYPE_DELETE {
|
||||
return
|
||||
}
|
||||
// 查询保存之前的数据
|
||||
if _, err := b.Get(&old); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
builder.After(func(b *db.Builder, t string, result sql.Result, err error, data ...map[string]any) {
|
||||
if err != nil || t == db.TYPE_SELECT {
|
||||
return
|
||||
} else if num, err := result.RowsAffected(); num == 0 || err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
user = GetAnonymous()
|
||||
}
|
||||
|
||||
// 触发消息队列
|
||||
if err := do.MustInvoke[contracts.Queue](this.container).Publish(ResChangeRecordTopic, ResChangeRecordTopicPayload{
|
||||
Type: t,
|
||||
User: user,
|
||||
Res: this,
|
||||
Old: old,
|
||||
New: data,
|
||||
Result: result,
|
||||
}); err != nil {
|
||||
log.Println("Queue Publish Err:", ResChangeRecordTopic, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func NewVirtualResource(pRes Resource, code, name, sql string, fields []ResField) req.Resource {
|
||||
return &Resource{
|
||||
Uuid: code,
|
||||
PUuid: pRes.Uuid,
|
||||
Code: code,
|
||||
Name: name,
|
||||
IsResVirtual: true,
|
||||
Table: sql,
|
||||
Namespace: pRes.Namespace,
|
||||
Workspace: pRes.Workspace,
|
||||
IsHistoryRecord: false,
|
||||
HistoryCacheMax: 0,
|
||||
Fields: fields,
|
||||
Roles: nil,
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type Route struct {
|
||||
Exact bool `db:"exact"`
|
||||
Platform string `db:"platform"`
|
||||
Extra map[string]any `db:"extra"`
|
||||
Roles []string `db:"roles"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
+7
-82
@@ -1,88 +1,13 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"github.com/samber/lo"
|
||||
|
||||
"git.fsdpf.net/go/req"
|
||||
"git.fsdpf.net/go/req/userx"
|
||||
)
|
||||
|
||||
type user struct {
|
||||
id int64
|
||||
uuid string
|
||||
username string
|
||||
nickname string
|
||||
roles []string
|
||||
runtime req.UserRuntime
|
||||
}
|
||||
// GetAnonymous 获取匿名用户。实现搬到了 req/userx(req 自己的子包,不反过来依赖 contracts),
|
||||
// 这里保留只是兼容原有调用方,函数本身没法用 type = 起别名(type 只能别名类型,不能别名函数),
|
||||
// 用 var 绑同一个函数值效果等价。
|
||||
var GetAnonymous = userx.GetAnonymous
|
||||
|
||||
func (this user) ID() int64 {
|
||||
return this.id
|
||||
}
|
||||
|
||||
func (this user) Uuid() string {
|
||||
return this.uuid
|
||||
}
|
||||
|
||||
func (this user) Username() string {
|
||||
return this.username
|
||||
}
|
||||
|
||||
func (this user) Nickname() string {
|
||||
return this.nickname
|
||||
}
|
||||
|
||||
func (this user) GetUserInfo() map[string]any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this user) Roles() (roles []string) {
|
||||
return this.roles
|
||||
}
|
||||
|
||||
func (this user) HasUserRoles(roles ...string) bool {
|
||||
return lo.Contains(this.Roles(), "ffffffff-ffff-ffff-ffff-ffffffffffff") || len(lo.Intersect(this.Roles(), roles)) > 0
|
||||
}
|
||||
|
||||
func (this user) IsAnonymous() bool {
|
||||
return this.id == 0
|
||||
}
|
||||
|
||||
func (this user) Runtime() req.UserRuntime {
|
||||
return this.runtime
|
||||
}
|
||||
|
||||
// 获取匿名用户
|
||||
func GetAnonymous(opts ...req.UserRuntimeOption) req.User {
|
||||
u := user{
|
||||
id: 0,
|
||||
uuid: "00000000-0000-0000-0000-000000000000",
|
||||
username: "anonymous",
|
||||
nickname: "匿名者",
|
||||
roles: []string{"00000000-0000-0000-0000-000000000000"},
|
||||
runtime: req.UserRuntime{},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&u.runtime)
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// 系统用户
|
||||
func GetSystemUser(opts ...req.UserRuntimeOption) req.User {
|
||||
u := user{
|
||||
id: -1,
|
||||
uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
||||
username: "system",
|
||||
nickname: "系统",
|
||||
roles: []string{"ffffffff-ffff-ffff-ffff-ffffffffffff"},
|
||||
runtime: req.UserRuntime{},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&u.runtime)
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
// GetSystemUser 系统用户,同上。
|
||||
var GetSystemUser = userx.GetSystemUser
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
type UserAsset struct {
|
||||
ID int64 `db:"id"`
|
||||
Name string `db:"name"`
|
||||
FileName string `db:"filename"`
|
||||
FileUrl string `db:"file_url"`
|
||||
File string `db:"file"`
|
||||
Mime string `db:"mime"`
|
||||
Size int64 `db:"size"`
|
||||
Ext string `db:"ext"`
|
||||
Hit int64 `db:"hit"`
|
||||
IsPrivate bool `db:"isPrivate"`
|
||||
UpdatedAt string `db:"updated_at"`
|
||||
CreatedAt string `db:"created_at"`
|
||||
}
|
||||
|
||||
type GetUserAsset func(user req.User, code string) (UserAsset, bool)
|
||||
|
||||
type UploadUserAsset func(user req.User, file string, name string) (UserAsset, error)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/reflux"
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
type CondflowService interface {
|
||||
Run(name string, input reflux.R, u req.User) error
|
||||
}
|
||||
|
||||
type FlowDecision interface {
|
||||
Controller
|
||||
Res() req.Resource
|
||||
}
|
||||
+12
-22
@@ -1,35 +1,25 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"git.fsdpf.net/go/req"
|
||||
"github.com/samber/do"
|
||||
)
|
||||
|
||||
// 日志结构化字段的标准 key:LogKeyKind 标识控制器大类(Http/Cron/Job 等),
|
||||
// LogKeyComponent 标识具体是哪一个实例/类型。
|
||||
const (
|
||||
LogKeyKind = "kind"
|
||||
LogKeyComponent = "component"
|
||||
)
|
||||
|
||||
type Controller interface {
|
||||
// 初始化
|
||||
Init() error
|
||||
// 获取 Container
|
||||
Container() *do.Injector
|
||||
Container() Injector
|
||||
// 派遣一个任务
|
||||
Dispatch(job string, payload any, u req.User) error
|
||||
}
|
||||
|
||||
type BaseController struct {
|
||||
container *do.Injector
|
||||
}
|
||||
|
||||
func (this BaseController) Container() *do.Injector {
|
||||
return this.container
|
||||
}
|
||||
|
||||
func (BaseController) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this BaseController) Dispatch(job string, payload any, u req.User) error {
|
||||
return do.MustInvoke[JobService](this.Container()).Dispatch(job, payload, u)
|
||||
}
|
||||
|
||||
func NewBaseController(container *do.Injector) Controller {
|
||||
return &BaseController{container}
|
||||
// Logger 返回当前进程的默认 slog logger
|
||||
Logger() *slog.Logger
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import "git.fsdpf.net/go/req"
|
||||
|
||||
type CronService interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
Shutdown() error
|
||||
Restart() error
|
||||
}
|
||||
|
||||
@@ -14,10 +14,3 @@ type Cron interface {
|
||||
Run(req.User) error
|
||||
}
|
||||
|
||||
type CronBase struct {
|
||||
Controller
|
||||
}
|
||||
|
||||
func (CronBase) Run(req.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package contracts
|
||||
|
||||
import "github.com/samber/do/v2"
|
||||
|
||||
type Injector = do.Injector
|
||||
|
||||
func Invoke[T any](i Injector) (T, error) {
|
||||
return do.Invoke[T](i)
|
||||
}
|
||||
|
||||
func MustInvoke[T any](i Injector) T {
|
||||
return do.MustInvoke[T](i)
|
||||
}
|
||||
|
||||
func InvokeNamed[T any](i Injector, name string) (T, error) {
|
||||
return do.InvokeNamed[T](i, name)
|
||||
}
|
||||
|
||||
func MustInvokeNamed[T any](i Injector, name string) T {
|
||||
return do.MustInvokeNamed[T](i, name)
|
||||
}
|
||||
@@ -101,6 +101,7 @@ var (
|
||||
// 请求第三个接口
|
||||
ErrApiRequest = &Errno{Code: 20501, Msg: "Api Request Error"}
|
||||
ErrApiResponsed = &Errno{Code: 20502, Msg: "Api Responsed Error"}
|
||||
ErrApiAuth = &Errno{Code: 20503, Msg: "Api Auth Error"}
|
||||
|
||||
// 验证码错误,206
|
||||
ErrCaptcha = &Errno{Code: 20601, Msg: "验证码生成器不存在"}
|
||||
@@ -110,7 +111,15 @@ var (
|
||||
ErrCaptchaSend = &Errno{Code: 20605, Msg: "验证码发送错误"}
|
||||
ErrCaptchaInterval = &Errno{Code: 20606, Msg: "验证码获取失败, 请稍后再试"}
|
||||
|
||||
//
|
||||
// Pkg 错误
|
||||
ErrPkgUnknown = &Errno{Code: 20701, Msg: "pkg unknown"}
|
||||
ErrPkgInvalid = &Errno{Code: 20702, Msg: "pkg invalid"}
|
||||
ErrPkgPlatformInvalid = &Errno{Code: 20703, Msg: "pkg platform invalid"}
|
||||
ErrPkgUpgrade = &Errno{Code: 20704, Msg: "pkg upgrade fail"}
|
||||
ErrPkgRollback = &Errno{Code: 20705, Msg: "pkg version rollback fail"}
|
||||
ErrPkgDownload = &Errno{Code: 20706, Msg: "pkg download fail"}
|
||||
ErrPkgUpload = &Errno{Code: 20707, Msg: "pkg upload fail"}
|
||||
ErrPkgBuild = &Errno{Code: 20708, Msg: "pkg build fail"}
|
||||
)
|
||||
|
||||
// 使用 错误码 和 error 创建新的 错误
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
type EventStream interface {
|
||||
Controller
|
||||
// 获取请求信息
|
||||
Request() *http.Request
|
||||
// 请求处理
|
||||
Execute(req.GlobalParams) error
|
||||
// 获取用户信息
|
||||
User() req.User
|
||||
// 路由信息
|
||||
Route() req.Route
|
||||
// 发送数据
|
||||
Sent(data, event string) error
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
type Executor interface {
|
||||
Controller
|
||||
Res() req.Resource
|
||||
}
|
||||
@@ -1,56 +1,65 @@
|
||||
module git.fsdpf.net/go/contracts
|
||||
|
||||
go 1.21
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509034445-afc243ea916e
|
||||
git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/golang/protobuf v1.5.2
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/jhump/protoreflect v1.15.1
|
||||
github.com/lestrrat-go/jwx v1.2.25
|
||||
github.com/samber/do v1.6.0
|
||||
github.com/samber/lo v1.39.0
|
||||
github.com/spf13/cast v1.6.0
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/tidwall/gjson v1.17.1
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
google.golang.org/grpc v1.52.0
|
||||
git.fsdpf.net/go/condition v0.0.0-20260821005829-d6c354769032
|
||||
git.fsdpf.net/go/db v0.0.0-20260820090948-9c6bb5a83508
|
||||
git.fsdpf.net/go/reflux v0.0.0-20260820092857-1f48635da3f1
|
||||
git.fsdpf.net/go/req v0.0.0-20260820100114-1d95bf64a2a0
|
||||
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff
|
||||
github.com/google/jsonschema-go v0.4.3
|
||||
github.com/mark3labs/mcp-go v0.54.1
|
||||
github.com/samber/do/v2 v2.0.0
|
||||
github.com/sashabaranov/go-openai v1.41.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.42 // indirect
|
||||
github.com/samber/go-type-to-string v1.8.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758 // indirect
|
||||
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff // indirect
|
||||
github.com/bufbuild/protocompile v0.4.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/goccy/go-json v0.9.7 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.12 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang/protobuf v1.5.2
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jhump/protoreflect v1.15.1
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.1 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.25
|
||||
github.com/lestrrat-go/option v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/tidwall/gjson v1.17.1 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect
|
||||
google.golang.org/grpc v1.52.0
|
||||
google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -36,36 +36,20 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509022115-37018837d1b0 h1:MHjkN+zrSCqb7Qswn9fFrHT4XaoDnGbXz0ssSKnWnQk=
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509022115-37018837d1b0/go.mod h1:Zj9yrMa/A3WICTc9zn3W55ociuoBompEsVPo6EUXW5Q=
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509023514-a871ccfa75ac h1:pJQ0sFjPtv/RCy8k0mC/Kpn/j6As5bSqXyG0MWaylhc=
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509023514-a871ccfa75ac/go.mod h1:Zj9yrMa/A3WICTc9zn3W55ociuoBompEsVPo6EUXW5Q=
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509024751-9694d9033266 h1:s5On5PKF1ZiIGgq6R2Nk/2GcURuZxDAVK1Sru54Ybn0=
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509024751-9694d9033266/go.mod h1:Zj9yrMa/A3WICTc9zn3W55ociuoBompEsVPo6EUXW5Q=
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509030058-b0e337133e44 h1:XHJxWJuofEL7TLMDBsywi48nciaon8bEFOJLMYkOrNk=
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509030058-b0e337133e44/go.mod h1:xgX0fc3LEgrPk9tihGQqoQND4EWlNgYbLCi70TuCvvM=
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509034445-afc243ea916e h1:ZfwjqZS5URF4Y0JfQeytSdmXimJ4l74ooWRppza+3Tk=
|
||||
git.fsdpf.net/go/condition v0.0.0-20240509034445-afc243ea916e/go.mod h1:K5ocyKVxgC5mI1k/crgrEozK0r51qAavWg0hweXo6PU=
|
||||
git.fsdpf.net/go/db v0.0.0-20230621051209-5740d112407f h1:H+/H6j+hyFMgSy+tUNmymRy7/PWQRjjp8qY/DrhwrsY=
|
||||
git.fsdpf.net/go/db v0.0.0-20230621051209-5740d112407f/go.mod h1:397Sdx1cJS0OlHtTX1bVl//9k3Xn0Klnc6jC4MAkb6w=
|
||||
git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b h1:fRgWNOQ9dAYuUZHQ24oi1XqRbJIcJvZGbnQDaAKI7IY=
|
||||
git.fsdpf.net/go/db v0.0.0-20230731125324-11651ea6640b/go.mod h1:397Sdx1cJS0OlHtTX1bVl//9k3Xn0Klnc6jC4MAkb6w=
|
||||
git.fsdpf.net/go/req v0.0.0-20240508133526-672fc634ef20 h1:PhjRtac2r0Vn1WBBzhRhz4//bJhHgkf/SHZe3mQYPto=
|
||||
git.fsdpf.net/go/req v0.0.0-20240508133526-672fc634ef20/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
|
||||
git.fsdpf.net/go/req v0.0.0-20240509031332-f00b993f9c7d h1:TKNhQWDKyL50eVC/ptur1YuRkjRnKOlMsE1Y9tFCVbk=
|
||||
git.fsdpf.net/go/req v0.0.0-20240509031332-f00b993f9c7d/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
|
||||
git.fsdpf.net/go/req v0.0.0-20240509031626-6501b5ed0f2b h1:KlVBAuWnFt1lcRTCcBU165MIldJqleO/LahWrd3FvUA=
|
||||
git.fsdpf.net/go/req v0.0.0-20240509031626-6501b5ed0f2b/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
|
||||
git.fsdpf.net/go/req v0.0.0-20240509033523-c8cda2d8db56 h1:7ElgXUzt75brzOr44mItipTQTk++kZNoGOyTWHLxkTU=
|
||||
git.fsdpf.net/go/req v0.0.0-20240509033523-c8cda2d8db56/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
|
||||
git.fsdpf.net/go/req v0.0.0-20240509033936-f8a11598db60 h1:hHrEcUSbtajC2LAo2fvEo7yrSSDeM/s5FAuQluP7yZQ=
|
||||
git.fsdpf.net/go/req v0.0.0-20240509033936-f8a11598db60/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
|
||||
git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758 h1:nHys8Bwb29b/FvnE100KVH62aD2j5CEPxTQ/RRHo27k=
|
||||
git.fsdpf.net/go/req v0.0.0-20240509034400-4ef8c130a758/go.mod h1:S+p7t3XclKxsvaXQng7AMmqEGq11FSnMzxOtv1z8JUw=
|
||||
git.fsdpf.net/go/condition v0.0.0-20260821005829-d6c354769032 h1:6sLpt4Ste4Kfr0GLJRHYTQInodMOUOK5dm4iVC1CNQU=
|
||||
git.fsdpf.net/go/condition v0.0.0-20260821005829-d6c354769032/go.mod h1:fgyIm04l+hAGv6Z8oKQqX61CATpzw7D2qaxD1x2ffmA=
|
||||
git.fsdpf.net/go/db v0.0.0-20260820090948-9c6bb5a83508 h1:xv2SV/Qb0TKJFnzfBtSKSeXwa6NZPRBVoGfWDahsezo=
|
||||
git.fsdpf.net/go/db v0.0.0-20260820090948-9c6bb5a83508/go.mod h1:oDPmvCdZn/vTpbuPYPYkjdrhpis/XtMIi3eni4sYRvM=
|
||||
git.fsdpf.net/go/reflux v0.0.0-20260820092857-1f48635da3f1 h1:TRn++7oWwtANQM5JWTQGdW6fAkWMIbIsZX6tzmnsrLQ=
|
||||
git.fsdpf.net/go/reflux v0.0.0-20260820092857-1f48635da3f1/go.mod h1:8OLCLMUwdsZ8u2sRI9y/qaE4fTf5d0SrCJ6Zn+lttFY=
|
||||
git.fsdpf.net/go/req v0.0.0-20260820100114-1d95bf64a2a0 h1:HWM1soZb13IIQA69M8DgyLYu9goz7PArtWI2mQay918=
|
||||
git.fsdpf.net/go/req v0.0.0-20260820100114-1d95bf64a2a0/go.mod h1:Lm8WoA8EYs2696rmlxpySoCQY26Qh97cDxVF6NLBfZ8=
|
||||
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff h1:1hokQGKGIstoI7WDBir2N24OGgnussZ3GmucK4TQRuI=
|
||||
git.fsdpf.net/go/utils v0.0.0-20240509025914-c03a9cb48aff/go.mod h1:NUoyQtFr905YT+pi850mvSf4YX0WghQIcMQkTvize5o=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -82,26 +66,28 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -142,7 +128,10 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
|
||||
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -158,8 +147,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
@@ -178,10 +167,12 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
|
||||
@@ -194,10 +185,14 @@ github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0ot
|
||||
github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
|
||||
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mark3labs/mcp-go v0.54.1 h1:Ap/ptEB9FtWzFKM8NDsTA7QDxerQOC06eZigrTldVj0=
|
||||
github.com/mark3labs/mcp-go v0.54.1/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas=
|
||||
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
|
||||
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
@@ -209,19 +204,22 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/samber/do v1.6.0 h1:Jy/N++BXINDB6lAx5wBlbpHlUdl0FKpLWgGEV9YWqaU=
|
||||
github.com/samber/do v1.6.0/go.mod h1:DWqBvumy8dyb2vEnYZE7D7zaVEB64J45B0NjTlY/M4k=
|
||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/samber/do/v2 v2.0.0 h1:tnunwWaoqSfJ9hxVIaJawIo7JXHQlqT9d9YBXlE9Keg=
|
||||
github.com/samber/do/v2 v2.0.0/go.mod h1:ZSBCE7Xr6nTNIOVo4DBrkl2+ydUbIOzJjjdV8En5XO4=
|
||||
github.com/samber/go-type-to-string v1.8.0 h1:5z6tDTjtXxkIAoAuHAZYMYR8mkBZjVgeSH7jcSLqc8w=
|
||||
github.com/samber/go-type-to-string v1.8.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
|
||||
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@@ -231,6 +229,8 @@ github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jH
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
@@ -238,13 +238,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -253,6 +252,8 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -271,8 +272,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -283,8 +284,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -340,8 +339,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -361,7 +360,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -398,8 +398,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -408,8 +408,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -561,8 +561,9 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
||||
google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743 h1:yqElulDvOF26oZ2O+2/aoX7mQ8DY/6+p39neytrycd8=
|
||||
google.golang.org/protobuf v1.28.2-0.20230222093303-bc1253ad3743/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package contracts
|
||||
|
||||
type GoBridgeService interface {
|
||||
Start() error
|
||||
Shutdown() error
|
||||
Restart() error
|
||||
}
|
||||
|
||||
// GoBridge 继承 Controller,可通过 gobridge.WithHandlers 绑定到 Pool,
|
||||
// 所有公开方法自动暴露给 Python 通过 call_go() 调用。
|
||||
type GoBridge interface {
|
||||
Controller
|
||||
}
|
||||
@@ -16,10 +16,3 @@ type GRpc interface {
|
||||
GetGRpcServiceDesc() *grpc.ServiceDesc
|
||||
}
|
||||
|
||||
type GRpcBase struct {
|
||||
Controller
|
||||
}
|
||||
|
||||
func (GRpcBase) GetGRpcServiceDesc() *grpc.ServiceDesc {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,77 +1,66 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.fsdpf.net/go/req"
|
||||
"github.com/samber/do"
|
||||
)
|
||||
|
||||
var defaultWsClientGroup WsClientGroup = "__DEFAULT__"
|
||||
var wsClientID WsClientID = 0
|
||||
|
||||
type Http interface {
|
||||
type HTTPService interface {
|
||||
// Start 启动 HTTP 服务。
|
||||
Start() error
|
||||
Stop() error
|
||||
|
||||
// Shutdown 停止 HTTP 服务,等待当前请求处理完成后退出。
|
||||
Shutdown() error
|
||||
|
||||
// Restart 重启 HTTP 服务,先停止再重新加载路由并启动。
|
||||
Restart() error
|
||||
|
||||
// Invoke 在进程内直接调用 HTTP 路由处理器,不通过网络。
|
||||
//
|
||||
// 参数:
|
||||
// - method: HTTP 方法,如 "GET"、"POST" 等。
|
||||
// - path: 请求路径,如 "/api/user/list"。
|
||||
// - body: 请求体,无请求体时传 nil。
|
||||
// - headers: 附加请求头键值对,无需额外请求头时传 nil。
|
||||
// - caller: 指定认证中间件应认证成的调用者身份,为 nil 时按 headers 里的
|
||||
// Authorization/token 正常解析(等同于外部请求)。只有进程内部
|
||||
// 发起调用时才可能传非 nil 值,不受外部请求影响。
|
||||
//
|
||||
// 返回:
|
||||
// - *http.Response: 包含状态码、响应头及响应体的结果。
|
||||
// - error: 构造请求失败时返回错误。
|
||||
Invoke(method, path string, body io.Reader, header http.Header, caller req.User) (*http.Response, error)
|
||||
}
|
||||
|
||||
type HttpController interface {
|
||||
Controller
|
||||
|
||||
// 获取用户信息
|
||||
// User 返回当前请求的认证用户信息。
|
||||
User() req.User
|
||||
|
||||
// 获取请求信息
|
||||
// Request 返回当前 HTTP 请求对象。
|
||||
Request() *http.Request
|
||||
|
||||
// 请求处理
|
||||
// Execute 处理请求并返回响应数据。
|
||||
Execute(req.GlobalParams) any
|
||||
|
||||
// 路由信息
|
||||
// Route 返回当前请求匹配到的路由信息。
|
||||
Route() req.Route
|
||||
|
||||
// 内部调用
|
||||
Call(code string, params map[string]any, category ...req.RouteCategory) (req.HttpResponse, error)
|
||||
// 内部调用
|
||||
// Invoke()
|
||||
// Call 在进程内以路由 code 发起内部调用,等同于向自身发送 HTTP 请求。
|
||||
//
|
||||
// 参数:
|
||||
// - code: 路由标识码。
|
||||
// - params: 附加请求参数。
|
||||
// - category: 可选的路由匹配选项。
|
||||
//
|
||||
// 返回:
|
||||
// - req.HttpResponse: 内部调用的响应结果。
|
||||
// - error: 调用失败时返回错误。
|
||||
Call(code string, params map[string]any, category ...req.RouteMatchOption) (req.HttpResponse, error)
|
||||
|
||||
// 数据库权限
|
||||
AuthDB() req.ResAuthDB
|
||||
}
|
||||
|
||||
type HttpHandleController struct {
|
||||
Controller
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
func (this HttpHandleController) Request() *http.Request {
|
||||
return this.request
|
||||
}
|
||||
|
||||
func (this HttpHandleController) Route() req.Route {
|
||||
return this.Request().Context().Value(req.RouteCtx{Name: "Route"}).(req.Route)
|
||||
}
|
||||
|
||||
func (this HttpHandleController) User() req.User {
|
||||
return this.Request().Context().Value(req.RouteCtx{Name: "User"}).(req.User)
|
||||
}
|
||||
|
||||
func (this HttpHandleController) Call(code string, params map[string]any, category ...req.RouteCategory) (req.HttpResponse, error) {
|
||||
return do.MustInvoke[req.Router](this.Container()).Call(this.Request(), code, params, category...)
|
||||
}
|
||||
|
||||
func (HttpHandleController) Execute(params req.GlobalParams) any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (HttpHandleController) AuthDB() req.ResAuthDB {
|
||||
return req.ResAuthOn
|
||||
}
|
||||
|
||||
func NewHttpController(container *do.Injector, request *http.Request) HttpController {
|
||||
return &HttpHandleController{
|
||||
Controller: &BaseController{container},
|
||||
request: request,
|
||||
}
|
||||
// AuthDB 返回当前控制器的数据库权限模式。
|
||||
AuthDB() req.ResFlags
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import "git.fsdpf.net/go/req"
|
||||
|
||||
type JobService interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
Shutdown() error
|
||||
Restart() error
|
||||
Dispatch(job string, payload any, u req.User) error
|
||||
}
|
||||
@@ -13,12 +13,18 @@ type Job interface {
|
||||
Controller
|
||||
// 任务处理
|
||||
Handle(any, req.User) error
|
||||
// OnExceeded 重试次数耗尽时由框架回调,用于清理状态。默认实现为空操作。
|
||||
OnExceeded(any, req.User) error
|
||||
}
|
||||
|
||||
type JobBase struct {
|
||||
Controller
|
||||
// NoRetryError 表示不可重试的错误,job 返回此类型后框架立即标记为 exceeded,不再调度重试。
|
||||
type NoRetryError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (JobBase) Handle(any, req.User) error {
|
||||
return nil
|
||||
}
|
||||
func (e *NoRetryError) Error() string { return e.Err.Error() }
|
||||
func (e *NoRetryError) Unwrap() error { return e.Err }
|
||||
|
||||
// ErrNoRetry 将 err 包装为不可重试错误,job handler 使用此函数终止重试。
|
||||
func ErrNoRetry(err error) error { return &NoRetryError{Err: err} }
|
||||
|
||||
|
||||
+2
-2
@@ -4,11 +4,11 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
"github.com/samber/do"
|
||||
"github.com/samber/do/v2"
|
||||
)
|
||||
|
||||
type JWTAuth interface {
|
||||
HttpMiddleware(container *do.Injector) func(next http.Handler) http.Handler
|
||||
HttpMiddleware(container do.Injector) func(next http.Handler) http.Handler
|
||||
Encode(claims map[string]interface{}) (token string, err error)
|
||||
Decode(token string) (jwt.Token, error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.fsdpf.net/go/req"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
openai "github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
// InjectOption 是 InjectTools 的可选配置项。
|
||||
type InjectOption func(*InjectOptions)
|
||||
|
||||
type InjectOptions struct {
|
||||
Codes []string
|
||||
OnCall func(openai.ToolCall) // 工具执行前回调
|
||||
OnResult func(openai.ToolCall, string) // 工具执行后回调
|
||||
}
|
||||
|
||||
// WithFilter 只注入指定 apiCode 的 MCP server 工具;不传则注入全部已注册的。
|
||||
func WithFilter(codes ...string) InjectOption {
|
||||
return func(o *InjectOptions) { o.Codes = codes }
|
||||
}
|
||||
|
||||
// WithOnCall 设置工具执行前回调,tc 为完整的 ToolCall 对象。
|
||||
func WithOnCall(fn func(tc openai.ToolCall)) InjectOption {
|
||||
return func(o *InjectOptions) { o.OnCall = fn }
|
||||
}
|
||||
|
||||
// WithOnResult 设置工具执行后回调,tc 为 ToolCall 对象,result 为返回文本。
|
||||
func WithOnResult(fn func(tc openai.ToolCall, result string)) InjectOption {
|
||||
return func(o *InjectOptions) { o.OnResult = fn }
|
||||
}
|
||||
|
||||
type MCPService interface {
|
||||
Serve(req.Route) MCPServer
|
||||
Shutdown() error
|
||||
// InjectTools 将工具列表注入 ChatCompletionRequest.Tools,同时返回 Instructions 和工具调用执行器。
|
||||
// ctx 须携带 req.User(WithToolFilter 据此过滤工具)。
|
||||
// 通过 WithOnCall / WithOnResult 设置实时进度回调,WithFilter 限定注入范围。
|
||||
InjectTools(ctx context.Context, impl mcp.Implementation, req *openai.ChatCompletionRequest, opts ...InjectOption) (
|
||||
instructions string,
|
||||
handleToolCalls func(toolCalls []openai.ToolCall) []openai.ChatCompletionMessage,
|
||||
err error,
|
||||
)
|
||||
}
|
||||
|
||||
type MCPServer interface {
|
||||
Server
|
||||
Shutdown() error
|
||||
}
|
||||
|
||||
type MCPTool interface {
|
||||
Controller
|
||||
// 获取 MCPServer
|
||||
MCP() *server.MCPServer
|
||||
// Sent 在工具执行期间通过 notifications/progress 向客户端推送进度消息。
|
||||
// 客户端请求须携带 _meta.progressToken,否则本方法为空操作。
|
||||
Sent(ctx context.Context, data string) error
|
||||
// Execute 处理请求并返回响应数据,返回值支持以下类型:
|
||||
// - mcp.Content / []mcp.Content 直接作为工具内容返回
|
||||
// - *mcp.CallToolResult 完整结果,直接透传
|
||||
// - contracts.Errno / *contracts.Errno 业务错误码,转为工具错误
|
||||
// - contracts.Err / *contracts.Err 同上
|
||||
// - string 文本结果
|
||||
// - []byte 文本结果(原始字节)
|
||||
// - error 工具错误
|
||||
// - nil 空结果
|
||||
// - 其他任意类型 JSON 序列化后返回,同时填充 StructuredContent
|
||||
Execute(context.Context, req.GlobalParams) any
|
||||
}
|
||||
|
||||
type MCPResource interface {
|
||||
Controller
|
||||
}
|
||||
@@ -2,12 +2,11 @@ package contracts
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/req"
|
||||
"github.com/samber/do"
|
||||
)
|
||||
|
||||
type MqttService interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
Shutdown() error
|
||||
Restart() error
|
||||
|
||||
// 消息推送
|
||||
@@ -33,9 +32,9 @@ type MqttService interface {
|
||||
type Mqtt interface {
|
||||
Controller
|
||||
// 链接
|
||||
// OnConnect(g GlobalParams, topic string) error
|
||||
OnConnect(g req.GlobalParams) error
|
||||
// 断开链接
|
||||
// OnDisconnect(g GlobalParams, topic string) error
|
||||
OnDisconnect(g req.GlobalParams) error
|
||||
// 订阅事件
|
||||
OnSubscribed(g req.GlobalParams, topic string) error
|
||||
// 取消订阅事件
|
||||
@@ -56,32 +55,3 @@ type Mqtt interface {
|
||||
// OnRetainedExpired(g GlobalParams, topic string) error
|
||||
}
|
||||
|
||||
type MqttHandle struct {
|
||||
Controller
|
||||
}
|
||||
|
||||
func (this *MqttHandle) OnSubscribed(g req.GlobalParams, topic string) error {
|
||||
return ErrFuncNotImplemented
|
||||
}
|
||||
|
||||
func (this MqttHandle) OnUnsubscribed(g req.GlobalParams, topic string) error {
|
||||
return ErrFuncNotImplemented
|
||||
}
|
||||
|
||||
func (this MqttHandle) OnMessage(g req.GlobalParams, topic string, retain bool, qos byte) error {
|
||||
return ErrFuncNotImplemented
|
||||
}
|
||||
|
||||
func (this MqttHandle) OnMessageDropped(g req.GlobalParams, topic string, retain bool, qos byte) error {
|
||||
return ErrFuncNotImplemented
|
||||
}
|
||||
|
||||
func (this MqttHandle) OnPublish(g req.GlobalParams, topic string, retain bool, qos byte, raw []byte) ([]byte, error) {
|
||||
return nil, ErrFuncNotImplemented
|
||||
}
|
||||
|
||||
func NewMqttController(container *do.Injector) Mqtt {
|
||||
return &MqttHandle{
|
||||
Controller: &BaseController{container},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"git.fsdpf.net/go/condition"
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/db/exp"
|
||||
"git.fsdpf.net/go/reflux"
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
@@ -12,39 +15,35 @@ type OrmExecute int
|
||||
|
||||
type RelationType string
|
||||
|
||||
type OrderByDirection string
|
||||
|
||||
const (
|
||||
OrmExecute_Query OrmExecute = iota
|
||||
OrmExecute_Show
|
||||
OrmExecute_Store
|
||||
OrmExecute_Destroy
|
||||
OrmQuery OrmExecute = iota
|
||||
OrmShow
|
||||
OrmStore
|
||||
OrmDestroy
|
||||
)
|
||||
|
||||
const (
|
||||
RelationType_HasOne RelationType = "hasOne"
|
||||
RelationType_HasMany RelationType = "hasMany"
|
||||
RelationType_Inner RelationType = "inner"
|
||||
RelationType_Left RelationType = "left"
|
||||
RelationType_Right RelationType = "right"
|
||||
OrmHasOne RelationType = "hasOne"
|
||||
OrmHasMany RelationType = "hasMany"
|
||||
OrmInnerJoin RelationType = "inner"
|
||||
OrmLeftJoin RelationType = "left"
|
||||
OrmRightJoin RelationType = "right"
|
||||
)
|
||||
|
||||
const (
|
||||
OrderByDirection_ASC OrderByDirection = db.ORDER_ASC
|
||||
OrderByDirection_DESC OrderByDirection = db.ORDER_DESC
|
||||
)
|
||||
type ModelDecorator = func(Model) Model
|
||||
|
||||
type Orm interface {
|
||||
SetGlobalParams(g req.GlobalParams) Orm
|
||||
SetGlobalParams(g req.UserAccessor) Orm
|
||||
GetModel(params ...string) Model
|
||||
SetRelationModel(m Model) Model
|
||||
SetQueryField(qf req.QueryField) error
|
||||
SetOrderBy(params []OrderBy) error
|
||||
SetCondition(cond *condition.Condition) error
|
||||
SetController(ctr OrmController) error
|
||||
Execute(OrmExecute) (any, error)
|
||||
Clone(tx *db.Transaction) Orm
|
||||
Count() int
|
||||
Controller() reflect.Value
|
||||
Execute(OrmExecute) (reflux.R, error)
|
||||
Clone(tx *db.TxDatabase) Orm
|
||||
Count() int64
|
||||
// Exists() bool
|
||||
}
|
||||
|
||||
@@ -54,7 +53,6 @@ type Relation interface {
|
||||
Type() RelationType
|
||||
SetType(RelationType)
|
||||
SetCondition(*condition.Condition) *condition.Condition
|
||||
SetPrependCondition(*condition.Condition) *condition.Condition
|
||||
GetCondition() *condition.Condition
|
||||
GetRelationResource() string
|
||||
GetRelationField() string
|
||||
@@ -66,57 +64,63 @@ type Join interface {
|
||||
GetCode() string
|
||||
GetResource() req.Resource
|
||||
// GetDependencies(items []ResRelation, dependencies ...string) []string
|
||||
Inject(dbBuilder *db.Builder, m Model)
|
||||
Inject(dbBuilder *db.SelectDataset, m Model)
|
||||
}
|
||||
|
||||
type Model interface {
|
||||
Relation
|
||||
condition.TokenValue
|
||||
IsDuplicated() bool
|
||||
GetCode() string
|
||||
GetPrimaryKey() string
|
||||
GetQueryFieldsStruct(extends ...reflect.StructField) any
|
||||
GetQueryFieldsSliceStruct(fields ...reflect.StructField) any
|
||||
GetAttribute() map[string]any
|
||||
GetResult() reflect.Value
|
||||
GetAttribute() reflux.R
|
||||
SetRelationAttribute(string, reflux.R) error
|
||||
GetResult() reflux.R
|
||||
GetResource() req.Resource
|
||||
QueryBuilder() *db.Builder
|
||||
GetTransaction() *db.Transaction
|
||||
SetQueryField(req.QueryField)
|
||||
SetRelationResult(code string, vItems reflect.Value)
|
||||
GetTransaction() *db.TxDatabase
|
||||
SetQueryField(...req.QueryField)
|
||||
// SetRelationResult(code string, items []reflux.R)
|
||||
GetJoinsCode() []string
|
||||
SetAuthDB(req.ResAuthDB)
|
||||
SetPermission(req.ResFlags)
|
||||
SetOrderBy(params ...OrderBy) Model
|
||||
SetGroupBy(params ...GroupBy) Model
|
||||
SetJoin(Join) Model
|
||||
SetLimit(int) Model
|
||||
SetOffset(int) Model
|
||||
SetAttribute(data any) Model
|
||||
SetLimit(uint) Model
|
||||
SetOffset(uint) Model
|
||||
// SetAttribute(data any) Model
|
||||
SetPrimaryKey(string) Model
|
||||
SetWithRecursive(QueryWithRecursive) Model
|
||||
DelQueryField(string)
|
||||
SelectDataset() *db.SelectDataset
|
||||
SelectDatasetWithUser(u req.User) *db.SelectDataset
|
||||
Query() error
|
||||
Store() error
|
||||
Destroy() error
|
||||
Count() (int, error)
|
||||
Count() (int64, error)
|
||||
// Exists() (bool, error)
|
||||
}
|
||||
|
||||
type OrmController interface {
|
||||
AuthDB() req.ResAuthDB
|
||||
AuthDB() req.ResFlags
|
||||
}
|
||||
|
||||
type OrderBy interface {
|
||||
ToSql() db.Expression
|
||||
Inject(dbBuilder *db.Builder, m Model)
|
||||
ToSql() exp.OrderedExpression
|
||||
Inject(sd *db.SelectDataset, m Model)
|
||||
}
|
||||
|
||||
type GroupBy interface {
|
||||
ToSql() db.Expression
|
||||
Inject(dbBuilder *db.Builder, m Model)
|
||||
ToSql() exp.ColumnListExpression
|
||||
Inject(sd *db.SelectDataset, m Model)
|
||||
}
|
||||
|
||||
type QueryWithRecursive interface {
|
||||
ToQueryBuilder(*db.Builder, Model) (*db.Builder, any)
|
||||
ToTreeData(any) any
|
||||
ToQueryBuilder(*db.SelectDataset, Model) (*db.SelectDataset, any)
|
||||
ToTreeData([]reflux.R) reflux.R
|
||||
// RootCondition 返回锚点条件(pField = root),供外部如 Count() 在不展开递归查询的情况下复用。
|
||||
RootCondition() exp.Expression
|
||||
}
|
||||
|
||||
// @title 创建关联模型
|
||||
@@ -129,18 +133,23 @@ type NewOrmRelation func(t RelationType, m Model, rResource, rField, rForeignKey
|
||||
|
||||
type NewOrmJoin func(t RelationType, res req.Resource, alias, rResource, rField, rForeignKey string) Join
|
||||
|
||||
type NewOrmQueryWithRecursive func(pField, cField string, root any, isWithoutCondition bool, depth int) QueryWithRecursive
|
||||
type NewOrmQueryWithRecursive func(pField, cField exp.IdentifierExpression, root any, isWithoutCondition bool, depth uint) QueryWithRecursive
|
||||
|
||||
type NewOrmOrderBy func(sql string, direction OrderByDirection) OrderBy
|
||||
type NewOrmOrderBy func(sql db.Expression, direction exp.SortDirection) OrderBy
|
||||
|
||||
type NewOrmGroupBy func(sql string) GroupBy
|
||||
type NewOrmGroupBy func(db.Expression) GroupBy
|
||||
|
||||
type NewOrmModel func(res req.Resource, code, name string) Model
|
||||
|
||||
type NewOrm func(res req.Resource, tx *db.Transaction) Orm
|
||||
type NewOrm func(res req.Resource, opts ...OrmOption) Orm
|
||||
|
||||
type GetOrmConditions func(categoryUuid string, opts ...condition.Option) *condition.Condition
|
||||
|
||||
type GetOrmOrderBy func(categoryUuid string) []OrderBy
|
||||
|
||||
type GetOrmGroupBy func(categoryUuid string) []GroupBy
|
||||
|
||||
type OrmOption func(Orm)
|
||||
type WithTx func(tx *db.TxDatabase) OrmOption
|
||||
type WithContext func(ctx context.Context) OrmOption
|
||||
type WithUserAccessor func(ua req.UserAccessor) OrmOption
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/req"
|
||||
"github.com/samber/do"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type ResEventService interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
Restart() error
|
||||
}
|
||||
|
||||
type ResListener interface {
|
||||
Insert(new map[string]any, u req.User, id int64) error
|
||||
Update(new map[string]any, u req.User, old map[string]any) error
|
||||
Delete(old map[string]any, u req.User) error
|
||||
GetResource() req.Resource
|
||||
GetCode() string
|
||||
}
|
||||
|
||||
type BaseListener struct {
|
||||
Container *do.Injector
|
||||
res req.Resource // 监听资源
|
||||
code string // code
|
||||
events []string // 监听动作
|
||||
}
|
||||
|
||||
func (this BaseListener) GetCode() string {
|
||||
return this.code
|
||||
}
|
||||
|
||||
func (this BaseListener) GetResource() req.Resource {
|
||||
return this.res
|
||||
}
|
||||
|
||||
func (this BaseListener) HasEvent(event string) bool {
|
||||
return lo.Contains(this.events, event)
|
||||
}
|
||||
|
||||
func (BaseListener) Insert(new map[string]any, u req.User, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (BaseListener) Update(new map[string]any, u req.User, old map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (BaseListener) Delete(old map[string]any, u req.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewBaseListener(code string, res req.Resource, container *do.Injector) *BaseListener {
|
||||
return &BaseListener{code: code, res: res, Container: container}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package res_type
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 解析map ----------------------------------
|
||||
type ResFieldByMap map[string]any
|
||||
|
||||
func (c *ResFieldByMap) Scan(value any) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(value.([]byte), c); err != nil {
|
||||
return fmt.Errorf("ResFieldByMap json.Unmarshal Error, %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ResFieldByMap) Value() (driver.Value, error) {
|
||||
b, err := json.Marshal(c)
|
||||
return string(b), err
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package res_type
|
||||
|
||||
type ResFieldByFloat float64
|
||||
@@ -1,3 +0,0 @@
|
||||
package res_type
|
||||
|
||||
type ResFieldByInteger int64
|
||||
@@ -1,38 +0,0 @@
|
||||
package res_type
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 切片 ------------------------------------
|
||||
type ResFieldByAnys []any
|
||||
|
||||
func (c *ResFieldByAnys) Scan(value any) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if rune('[') != rune(v[0]) {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(value.([]byte), c); err != nil {
|
||||
return fmt.Errorf("ResFieldByAnys json.Unmarshal Error, %s, %s", value, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (c ResFieldByAnys) Value() (driver.Value, error) {
|
||||
b, err := json.Marshal(c)
|
||||
return string(b), err
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package res_type
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// Number 类型 --------------------------------
|
||||
type ResFieldByNumber float64
|
||||
|
||||
func (this *ResFieldByNumber) Scan(value any) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch s := value.(type) {
|
||||
case []byte:
|
||||
*this = ResFieldByNumber(cast.ToFloat64(string(s)))
|
||||
default:
|
||||
*this = ResFieldByNumber(cast.ToFloat64(s))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this ResFieldByNumber) Value() (driver.Value, error) {
|
||||
return this, nil
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package res_type
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 字符串 -------------------------------------
|
||||
type ResFieldByString string
|
||||
|
||||
func (this *ResFieldByString) Scan(value any) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch s := value.(type) {
|
||||
case []byte:
|
||||
*this = ResFieldByString(s)
|
||||
default:
|
||||
*this = ResFieldByString(fmt.Sprintf("%v", s))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ResFieldByString) Value() (driver.Value, error) {
|
||||
return c, nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
// ResVirtualTableSetup 由 framework-v2 在 sqlite_vtable build tag 下注册到容器,负责向 SQLite
|
||||
// 注册虚拟表模块,具体说明见 req.ResVirtualTableSetup。
|
||||
type ResVirtualTableSetup = req.ResVirtualTableSetup
|
||||
|
||||
// ResVirtualTable 在 req.ResVirtualTable 基础上合并 Controller,使实现方可以访问 DI 容器等框架能力;
|
||||
// 具体方法说明见 req.ResVirtualTable。
|
||||
type ResVirtualTable interface {
|
||||
Controller
|
||||
req.ResVirtualTable
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/req/resx"
|
||||
)
|
||||
|
||||
// ResEventType 资源操作类型:读(SELECT)+ 写(INSERT/UPDATE/DELETE)
|
||||
type ResEventType = resx.ResEventType
|
||||
|
||||
const (
|
||||
ResEventSelect = resx.ResEventSelect
|
||||
ResEventInsert = resx.ResEventInsert
|
||||
ResEventUpdate = resx.ResEventUpdate
|
||||
ResEventDelete = resx.ResEventDelete
|
||||
)
|
||||
|
||||
// ResChangeRow 描述一次资源写操作中某一行的变更内容
|
||||
type ResChangeRow = resx.ResChangeRow
|
||||
|
||||
// ResWatcher 关注一个或多个资源写操作的处理器,通过 _res_listener 注册表按资源绑定,
|
||||
// 每次事件触发都会重新构造实例(见 framework-v2 的 handler.ResWatcherBase / internal/resource 的
|
||||
// watcherDispatcher.getAppWatcher)
|
||||
type ResWatcher interface {
|
||||
Controller
|
||||
// Handle 处理一次资源变更,row 自带 User()
|
||||
Handle(row ResChangeRow) error
|
||||
GetCode() string
|
||||
}
|
||||
+10
-4
@@ -1,9 +1,15 @@
|
||||
package contracts
|
||||
|
||||
import "git.fsdpf.net/go/req"
|
||||
import (
|
||||
"git.fsdpf.net/go/req"
|
||||
)
|
||||
|
||||
// 资源
|
||||
type GetResource func(code string) (req.Resource, bool)
|
||||
// GetResource 资源;类型定义搬到了 req.GetResource(req 的子包需要用它查资源,不能反过来
|
||||
// import contracts),这里保留别名兼容原有引用。
|
||||
type GetResource = req.GetResource
|
||||
|
||||
// 获取用户
|
||||
// MustResource 资源, 没有则抛出错误;类型定义搬到了 req.MustResource,这里保留别名兼容原有引用。
|
||||
type MustResource = req.MustResource
|
||||
|
||||
// GetUser 获取用户
|
||||
type GetUser func(uuid string, options ...req.UserRuntimeOption) (req.User, error)
|
||||
|
||||
+6
-10
@@ -8,8 +8,9 @@ import (
|
||||
"git.fsdpf.net/go/condition"
|
||||
"git.fsdpf.net/go/contracts"
|
||||
"git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/db/exp"
|
||||
"git.fsdpf.net/go/utils"
|
||||
"github.com/samber/do"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
@@ -17,14 +18,9 @@ import (
|
||||
type Operation struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Icon string `json:"icon"`
|
||||
Size string `json:"size"`
|
||||
Block bool `json:"block"`
|
||||
Ghost bool `json:"ghost"`
|
||||
Shape string `json:"shape"`
|
||||
Type string `json:"type"`
|
||||
PopConfirm any `json:"popConfirm,omitempty"`
|
||||
Confirm map[string]any `json:"confirm"`
|
||||
Widget string `json:"widget"`
|
||||
WidgetType string `json:"widgetType"`
|
||||
WidgetProps any `json:"widgetProps"`
|
||||
@@ -42,7 +38,7 @@ type OperationAccess struct {
|
||||
data map[string][]any
|
||||
}
|
||||
|
||||
func (o Operation) ToDBColumn(model contracts.Model, container *do.Injector) (column db.Expression, field reflect.StructField) {
|
||||
func (o Operation) ToDBColumn(model contracts.Model, container do.Injector) (column db.Expression, field reflect.StructField) {
|
||||
GetOrmConditions := do.MustInvoke[contracts.GetOrmConditions](container)
|
||||
|
||||
uuid := strings.ReplaceAll(o.Uuid, "-", "_")
|
||||
@@ -57,9 +53,9 @@ func (o Operation) ToDBColumn(model contracts.Model, container *do.Injector) (co
|
||||
}
|
||||
|
||||
if cond.IsEmpty() {
|
||||
column = db.Raw(pk + " as `u_" + uuid + "`")
|
||||
column = db.I(pk).As("u_" + uuid)
|
||||
} else {
|
||||
column = db.Raw("if(" + string(cond.ToSql(model)) + ", " + pk + ", 0) as `u_" + uuid + "`")
|
||||
column = exp.NewSQLFunctionExpression("IF", cond.ToSql(model), db.I(pk), 0).As("u_" + uuid)
|
||||
}
|
||||
|
||||
// 主键类型
|
||||
|
||||
+50
-83
@@ -13,24 +13,18 @@ import (
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type HttpStatusResponse struct {
|
||||
status int
|
||||
body req.HttpResponse
|
||||
}
|
||||
|
||||
type RawResponse struct {
|
||||
raw []byte
|
||||
}
|
||||
type JsonResponse struct {
|
||||
raw []byte
|
||||
}
|
||||
|
||||
type MsgResponse struct {
|
||||
code int
|
||||
msg string
|
||||
}
|
||||
|
||||
type ErrResponse struct {
|
||||
*MsgResponse
|
||||
stack []byte
|
||||
headers [][2]string
|
||||
raw []byte
|
||||
}
|
||||
|
||||
type FileResponse struct {
|
||||
headers [][2]string
|
||||
disposition string
|
||||
name string
|
||||
}
|
||||
@@ -41,11 +35,29 @@ var wsUpgrader = websocket.Upgrader{
|
||||
},
|
||||
}
|
||||
|
||||
func (this HttpStatusResponse) Get(path ...string) req.GlobalParams {
|
||||
if this.body == nil {
|
||||
return req.NewGlobalParams("", nil)
|
||||
}
|
||||
return this.body.Get(path...)
|
||||
}
|
||||
|
||||
func (this HttpStatusResponse) Send(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(this.status)
|
||||
if this.body != nil {
|
||||
this.body.Send(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (this RawResponse) Get(path ...string) req.GlobalParams {
|
||||
return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(this.raw), nil), req.NewGlobalParam(string(this.raw), nil).Get(strings.Join(path, ".")))
|
||||
return lo.Ternary(len(path) == 0, req.NewGlobalParams(string(this.raw), nil), req.NewGlobalParams(string(this.raw), nil).Get(strings.Join(path, ".")))
|
||||
}
|
||||
|
||||
func (this RawResponse) Send(w http.ResponseWriter, r *http.Request) {
|
||||
for i := 0; i < len(this.headers); i++ {
|
||||
w.Header().Set(this.headers[i][0], this.headers[i][1])
|
||||
}
|
||||
|
||||
if r.Header.Get("Upgrade") != "" && strings.ToLower(r.Header.Get("Upgrade")) == "websocket" {
|
||||
c, _ := wsUpgrader.Upgrade(w, r, nil)
|
||||
c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, string(this.raw)))
|
||||
@@ -55,96 +67,51 @@ func (this RawResponse) Send(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (this JsonResponse) Get(path ...string) req.GlobalParams {
|
||||
return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(this.raw), nil), req.NewGlobalParam(string(this.raw), nil).Get(strings.Join(path, ".")))
|
||||
}
|
||||
|
||||
func (this JsonResponse) Send(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Upgrade") != "" && strings.ToLower(r.Header.Get("Upgrade")) == "websocket" {
|
||||
c, _ := wsUpgrader.Upgrade(w, r, nil)
|
||||
c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, string(this.raw)))
|
||||
c.Close()
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(this.raw)
|
||||
}
|
||||
}
|
||||
|
||||
func (this MsgResponse) Get(path ...string) req.GlobalParams {
|
||||
if resp, err := json.Marshal(map[string]any{
|
||||
"code": this.code,
|
||||
"msg": this.msg,
|
||||
}); err == nil {
|
||||
return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(resp), nil), req.NewGlobalParam(string(resp), nil).Get(strings.Join(path, ".")))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this MsgResponse) Send(w http.ResponseWriter, r *http.Request) {
|
||||
if resp, err := json.Marshal(this.Get().Value()); err == nil {
|
||||
(&JsonResponse{raw: resp}).Send(w, r)
|
||||
} else {
|
||||
HttpResponse(err).Send(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (this ErrResponse) Get(path ...string) req.GlobalParams {
|
||||
if resp, err := json.Marshal(map[string]any{
|
||||
"code": this.code,
|
||||
"msg": this.msg,
|
||||
"error": string(this.stack),
|
||||
}); err == nil {
|
||||
return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(resp), nil), req.NewGlobalParam(string(resp), nil).Get(strings.Join(path, ".")))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this ErrResponse) Send(w http.ResponseWriter, r *http.Request) {
|
||||
if resp, err := json.Marshal(this.Get().Value()); err == nil {
|
||||
(&JsonResponse{raw: resp}).Send(w, r)
|
||||
} else {
|
||||
HttpResponse(err).Send(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (this FileResponse) Get(path ...string) req.GlobalParams {
|
||||
if resp, err := json.Marshal(map[string]any{
|
||||
"name": this.name,
|
||||
"disposition": this.disposition,
|
||||
}); err == nil {
|
||||
return lo.Ternary(len(path) == 0, req.NewGlobalParam(string(resp), nil), req.NewGlobalParam(string(resp), nil).Get(strings.Join(path, ".")))
|
||||
return lo.Ternary(len(path) == 0, req.NewGlobalParams(string(resp), nil), req.NewGlobalParams(string(resp), nil).Get(strings.Join(path, ".")))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this FileResponse) Send(w http.ResponseWriter, r *http.Request) {
|
||||
if this.Get("disposition").String() != "" {
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+this.Get("disposition").String()+`"`)
|
||||
}
|
||||
if _, err := os.Stat(this.Get("name").String()); err != nil && os.IsNotExist(err) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
} else {
|
||||
if this.Get("disposition").String() != "" {
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+this.Get("disposition").String()+`"`)
|
||||
}
|
||||
|
||||
for i := 0; i < len(this.headers); i++ {
|
||||
w.Header().Set(this.headers[i][0], this.headers[i][1])
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, this.Get("name").String())
|
||||
}
|
||||
}
|
||||
|
||||
func NewRawResponse(b []byte) req.HttpResponse {
|
||||
return &RawResponse{raw: b}
|
||||
func NewHttpStatusResponse(status int, body req.HttpResponse) req.HttpResponse {
|
||||
return &HttpStatusResponse{status: status, body: body}
|
||||
}
|
||||
|
||||
func NewRawResponse(b []byte, headers ...[2]string) req.HttpResponse {
|
||||
return &RawResponse{raw: b, headers: headers}
|
||||
}
|
||||
|
||||
func NewFileResponse(name string, disposition string, headers ...[2]string) req.HttpResponse {
|
||||
return &FileResponse{name: name, disposition: disposition, headers: headers}
|
||||
}
|
||||
|
||||
func NewJsonResponse(b []byte) req.HttpResponse {
|
||||
return &JsonResponse{raw: b}
|
||||
}
|
||||
|
||||
func NewFileResponse(name string, disposition string) req.HttpResponse {
|
||||
return &FileResponse{name: name, disposition: disposition}
|
||||
return NewRawResponse(b, [2]string{"Content-Type", "application/json"})
|
||||
}
|
||||
|
||||
func NewMsgResponse(msg string, code int) req.HttpResponse {
|
||||
return &MsgResponse{msg: msg, code: code}
|
||||
return NewJsonResponse([]byte(fmt.Sprintf(`{"code": %d, "msg": %q}`, code, msg)))
|
||||
}
|
||||
|
||||
func NewErrResponse(err *contracts.Err) req.HttpResponse {
|
||||
@@ -173,7 +140,7 @@ func HttpResponse(data any) req.HttpResponse {
|
||||
"msg": contracts.OK.Msg,
|
||||
"data": json.RawMessage(v),
|
||||
}); e == nil {
|
||||
return &JsonResponse{raw: b}
|
||||
return NewJsonResponse(b)
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
@@ -183,7 +150,7 @@ func HttpResponse(data any) req.HttpResponse {
|
||||
"msg": contracts.OK.Msg,
|
||||
"data": v,
|
||||
}); e == nil {
|
||||
return &JsonResponse{raw: b}
|
||||
return NewJsonResponse(b)
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
+3
-3
@@ -21,10 +21,10 @@ func GoPluginCore(core string, services ...any) {
|
||||
} else if fn, ok := f.(func(map[string]contracts.Controller) error); ok {
|
||||
fn(s)
|
||||
}
|
||||
case map[string]contracts.ResListener:
|
||||
if f, err := p.Lookup("RegListens"); err != nil {
|
||||
case map[string]contracts.ResWatcher:
|
||||
if f, err := p.Lookup("RegResWatchers"); err != nil {
|
||||
panic(err)
|
||||
} else if fn, ok := f.(func(map[string]contracts.ResListener) error); ok {
|
||||
} else if fn, ok := f.(func(map[string]contracts.ResWatcher) error); ok {
|
||||
fn(s)
|
||||
}
|
||||
case map[string]contracts.GRpc:
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ type UserApproval interface {
|
||||
// 判断发起审批请求权限
|
||||
HasUserRoles(req.User) bool
|
||||
// 获取审批列表标记字段值
|
||||
Execute(id int64, status UserApprovalExecute, r *http.Request, tx *db.Transaction) (current, next UserApprovalFlow, err error)
|
||||
Execute(id int64, status UserApprovalExecute, r *http.Request, tx *db.TxDatabase) (current, next UserApprovalFlow, err error)
|
||||
}
|
||||
|
||||
type UserApprovalFlow interface {
|
||||
|
||||
@@ -5,17 +5,12 @@ import (
|
||||
|
||||
"git.fsdpf.net/go/req"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/samber/do"
|
||||
)
|
||||
|
||||
type WsClientGroup string
|
||||
type WsClientID uint
|
||||
|
||||
// type WsClient struct {
|
||||
// User
|
||||
// Group WsClientGroup
|
||||
// Socket *websocket.Conn
|
||||
// }
|
||||
const DefaultWsClientGroup WsClientGroup = "__DEFAULT__"
|
||||
|
||||
type WsClient interface {
|
||||
req.User
|
||||
@@ -50,50 +45,7 @@ type WsController interface {
|
||||
// 获取 Ws 客户端
|
||||
Client() WsClient
|
||||
// 获取 Ws 标识
|
||||
ClientId(req.GlobalParams) WsClientID
|
||||
ClientID(req.GlobalParams) WsClientID
|
||||
// 获取 Ws 分组
|
||||
ClientGroup(req.GlobalParams) WsClientGroup
|
||||
}
|
||||
|
||||
type WsHandleController struct {
|
||||
Controller
|
||||
ws WsClient
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
func (this WsHandleController) Request() *http.Request {
|
||||
return this.request
|
||||
}
|
||||
|
||||
func (this WsHandleController) Route() req.Route {
|
||||
return this.Request().Context().Value(req.RouteCtx{Name: "Route"}).(req.Route)
|
||||
}
|
||||
|
||||
func (this WsHandleController) User() req.User {
|
||||
return this.Request().Context().Value(req.RouteCtx{Name: "User"}).(req.User)
|
||||
}
|
||||
|
||||
func (this WsHandleController) Execute(req.GlobalParams) error {
|
||||
return ErrFuncNotImplemented
|
||||
}
|
||||
|
||||
func (this WsHandleController) Client() WsClient {
|
||||
return this.ws
|
||||
}
|
||||
|
||||
func (WsHandleController) ClientId(req.GlobalParams) WsClientID {
|
||||
wsClientID++
|
||||
return wsClientID
|
||||
}
|
||||
|
||||
func (WsHandleController) ClientGroup(req.GlobalParams) WsClientGroup {
|
||||
return defaultWsClientGroup
|
||||
}
|
||||
|
||||
func NewWsController(container *do.Injector, request *http.Request, ws WsClient) WsController {
|
||||
return &WsHandleController{
|
||||
Controller: &BaseController{container},
|
||||
request: request,
|
||||
ws: ws,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user