feat: 升级 samber/do v1 → v2

This commit is contained in:
2026-05-15 09:04:12 +08:00
parent 7741f7584d
commit 9ad5d25836
47 changed files with 1456 additions and 684 deletions
+37
View File
@@ -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)
+23
View File
@@ -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
+25
View File
@@ -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)
+1
View File
@@ -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"`
+1
View File
@@ -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"`
}
+19 -16
View File
@@ -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"` // 主键
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
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)
+1
View File
@@ -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"`
+3 -1
View File
@@ -14,7 +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"` // 表单分组
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"`
+15 -9
View File
@@ -6,7 +6,9 @@ import (
"strings"
"git.fsdpf.net/go/contracts"
"github.com/samber/do"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/req"
"github.com/samber/do/v2"
)
type PkgVersion struct {
@@ -39,7 +41,7 @@ func (this PkgVersion) GetSourceMapPath(prefix ...string) string {
)
}
func (this PkgVersion) GetFile(app *do.Injector) (file string, err error) {
func (this PkgVersion) GetFile(app do.Injector, u req.User) (file string, err error) {
if this.Id == 0 {
return file, contracts.ErrPkgUnknown
}
@@ -50,23 +52,27 @@ func (this PkgVersion) GetFile(app *do.Injector) (file string, err error) {
return file, contracts.ErrResNotFound
}
_, err = res.GetDBTable().Where("id", this.Id).Value(&file, "file")
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) {
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")
// res, ok := do.MustInvoke[contracts.GetResource](app)("PkgVersion")
if !ok {
return file, contracts.ErrResNotFound
}
// if !ok {
// return file, contracts.ErrResNotFound
// }
_, err = res.GetDBTable().Where("id", this.Id).Value(&file, "sourcemap")
// _, err = res.GetDBTable().Where("id", this.Id).Value(&file, "sourcemap")
return file, err
}
+7 -8
View File
@@ -5,7 +5,6 @@ import (
"strings"
"unicode"
"git.fsdpf.net/go/contracts/res_type"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/req"
)
@@ -67,13 +66,13 @@ func (this *QueryField) ToSql() db.Expression {
if this.Alias() != "" {
if this.IsExpr() {
if this.GetCode() == "" {
return db.Raw("null as `" + this.Alias() + "`")
return db.V(nil).As(this.Alias())
}
return db.Raw("(" + this.GetCode() + ") as `" + this.Alias() + "`")
return db.L(this.GetCode()).As(this.Alias())
}
return db.Raw("`" + this.GetCodeResource() + "`.`" + this.GetCode() + "` as `" + this.Alias() + "`")
return db.T(this.GetCodeResource()).Col(this.GetCode()).As(this.Alias())
}
return db.Raw("`" + this.GetCodeResource() + "`.`" + this.GetCode() + "`")
return db.T(this.GetCodeResource()).Col(this.GetCode())
}
func (this *QueryField) ToStructField(tags ...string) reflect.StructField {
@@ -104,11 +103,11 @@ func (this *QueryField) ToStructField(tags ...string) reflect.StructField {
case req.ReqString:
typ = reflect.TypeOf(string(""))
case req.ReqNumber:
typ = reflect.TypeOf(res_type.ResFieldByNumber(0))
typ = reflect.TypeOf(float64(0))
case req.ReqInteger:
typ = reflect.TypeOf(res_type.ResFieldByInteger(0))
typ = reflect.TypeOf(int64(0))
case req.ReqFloat:
typ = reflect.TypeOf(res_type.ResFieldByFloat(0))
typ = reflect.TypeOf(float64(0))
case req.ReqBool:
typ = reflect.TypeOf(true)
case req.ReqArray:
+6 -6
View File
@@ -1,11 +1,11 @@
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"`
Code string `db:"code" json:"code"`
Name string `db:"name" json:"name"`
Category string `db:"category" json:"category"`
DataType string `db:"type" json:"type"`
IsRequired bool `db:"isRequired" json:"isRequired"`
}
type ResApi struct {
@@ -16,7 +16,7 @@ type ResApi struct {
PrimaryKey string `db:"primaryKey"`
ResourceUuid string `db:"resource_uuid"`
Method string `db:"method"`
Category string `db:"action"`
Action string `db:"action"`
Params []ResApiParam `db:"params"`
Roles []string `db:"roles"`
IsAuthDB int `db:"is_auth_db"` // 启用权限过滤
+4 -2
View File
@@ -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),
)
+43 -11
View File
@@ -1,16 +1,48 @@
package base
import (
"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.Value), false
}
for _, rv := range rc.RoleValues {
for _, role := range roles {
if value, exists := rv[role]; exists {
return reflux.New(value), true
}
}
}
// 没有找到匹配的角色值,返回默认值
return reflux.New(rc.Value), false
}
type GetResConfigure func(key string, roles ...string) (valuex.Accessor, bool)
+62 -54
View File
@@ -7,24 +7,24 @@ import (
"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"
)
// 资源字段
// ResField 资源字段
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"`
Uuid string `db:"uuid" json:"uuid"`
Name string `db:"name" json:"name"`
Code string `db:"code" json:"code"`
CodeResource string `db:"codeResource" json:"codeResource"`
DataType req.ResDataType `db:"data_type" json:"data_type"`
Length string `db:"length" json:"length"`
Comment string `db:"comment" json:"comment"`
Default string `db:"default" json:"default"`
Virtual bool `db:"virtual" json:"virtual"`
VirtualExpr string `db:"virtual_expr" json:"virtual_expr"`
}
func (this ResField) ToStructField(tags ...string) reflect.StructField {
@@ -42,7 +42,7 @@ func (this ResField) ToStructField(tags ...string) reflect.StructField {
req.ResTimestamp, req.ResDate, req.ResDatetime:
typ = reflect.TypeOf(string(""))
case req.ResInteger, req.ResSmallInteger:
typ = reflect.TypeOf(res_type.ResFieldByInteger(0))
typ = reflect.TypeOf(int64(0))
case req.ResDecimal:
typ = reflect.TypeOf(float64(0))
case req.ResBoolean:
@@ -62,6 +62,10 @@ func (this ResField) ToStructField(tags ...string) reflect.StructField {
}
}
func (this ResField) IsVirtual() bool {
return this.Virtual
}
func (this ResField) GetCode() string {
return this.Code
}
@@ -101,30 +105,16 @@ func (this ResField) GetQueryDataType() req.RouteParamType {
}
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 this.DataType == req.ResJson {
if v == nil {
if this.Default != "" && this.Default[0:1] == "[" {
return db.Raw("'[]'")
return db.V("[]")
} else if this.Default != "" && this.Default[0:1] == "{" {
return db.Raw("'{}'")
return db.V("{}")
} else if this.Default == "" {
return db.Raw("'{}'")
return db.V("{}")
}
return this.Default
return this.GetRawDefault()
}
if str, ok := v.(string); ok {
@@ -137,52 +127,61 @@ func (this ResField) ToValue(v any) any {
panic(fmt.Sprintf("%s, 类型转换错误, %s", this.Code, err))
}
}
return strings.Trim(cast.ToString(v), " ")
return v
}
func (this ResField) GetRawDefault(driver string) db.Expression {
func (this ResField) GetRawDefault() db.Expression {
if this.DataType == req.ResJson {
if this.Default != "" && this.Default[0:1] == "[" {
return db.Raw("'[]'")
return db.V("[]")
} else if this.Default != "" && this.Default[0:1] == "{" {
return db.Raw("'{}'")
return db.V("{}")
} else if this.Default == "" {
return db.Raw("'{}'")
return db.V("{}")
}
} else if this.DataType == req.ResBoolean {
if v, _ := strconv.ParseBool(this.Default); v {
return db.Raw("'1'")
return db.V(true)
}
return db.Raw("'0'")
return db.V(false)
}
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)
return db.L(this.Default[4:])
}
if this.Default == "" {
if this.GetDataType() == req.ResDate || this.GetDataType() == req.ResDatetime {
return db.Raw("NULL")
return db.V(nil)
}
return db.Raw("''")
return db.V("")
}
if strings.ToUpper(this.Default) == "CURRENT_TIMESTAMP" {
return db.Raw(this.Default)
return db.L(this.Default)
}
return db.Raw("'" + this.Default + "'")
return db.V(this.Default)
}
func (this ResField) ToBlueprint(table *schema.Blueprint) (temp *schema.ColumnDefinition) {
switch this.Code {
case "id":
return table.BigIncrements("id").AutoIncrement().Comment("ID")
case "enabled":
return table.Boolean("enabled").Default("1").Comment("是否有效")
case "created_user":
return table.Char("created_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("创建者")
case "owned_user":
return table.Char("owned_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("拥有者")
case "created_at":
return table.Timestamp("created_at").UseCurrent().Comment("创建时间")
case "updated_at":
return table.Timestamp("updated_at").UseCurrent().Default(db.L("ON UPDATE CURRENT_TIMESTAMP")).Comment("更新时间")
case "deleted_at":
return table.DateTime("deleted_at").Nullable().Comment("删除时间")
}
isNull := false
comment := this.Name
def := any(this.Default)
@@ -207,15 +206,15 @@ func (this ResField) ToBlueprint(table *schema.Blueprint) (temp *schema.ColumnDe
case "integer":
// integer 默认长度 11
temp = table.Integer(this.Code)
case "date", "dateTime":
case "date", "dateTime", "timestamp":
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)
if strings.ToUpper(this.Default) == "SQL:CURRENT_TIMESTAMP" {
def = db.L("CURRENT_TIMESTAMP")
} else if def == "" {
isNull = true
}
@@ -242,6 +241,15 @@ func (this ResField) ToBlueprint(table *schema.Blueprint) (temp *schema.ColumnDe
case "text":
temp = table.Text(this.Code)
isNull = true
case "vector":
temp = table.Vector(this.Code, cast.ToInt(this.Length))
isNull = false
default:
panic(fmt.Sprintf("Unknown type: %s", this.DataType))
}
if this.IsVirtual() {
temp.VirtualAs(this.VirtualExpr)
}
if isNull {
+30
View File
@@ -0,0 +1,30 @@
package base
import (
"testing"
"git.fsdpf.net/go/db/schema"
"git.fsdpf.net/go/req"
_ "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 := ResField{
Uuid: "00000000-0000-0000-0000-000000000000",
Name: "ID",
Code: "id",
CodeResource: "User",
DataType: req.ResInteger,
Length: "20",
Comment: "",
Default: "sql:AUTO_INCREMENT",
}
id.ToBlueprint(table)
})
}
-26
View File
@@ -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
}
+222 -249
View File
@@ -5,21 +5,26 @@ import (
"fmt"
"log"
"reflect"
"sync"
"unicode"
"github.com/samber/do"
"github.com/samber/do/v2"
"github.com/samber/lo"
"git.fsdpf.net/go/condition"
"git.fsdpf.net/go/contracts"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/engine"
"git.fsdpf.net/go/db/exec"
"git.fsdpf.net/go/db/exp"
"git.fsdpf.net/go/db/schema"
"git.fsdpf.net/go/req"
)
// 资源变更事件
// ResChangeEventTopic 资源变更事件
const ResChangeEventTopic = "res-change-event-topic"
// 资源变更数据
// ResChangeRecordTopic 资源变更数据
const ResChangeRecordTopic = "res-change-record-topic"
type ResChangeEventTopicPayload struct {
@@ -37,61 +42,72 @@ type ResChangeRecordTopicPayload struct {
New []map[string]any // 新数据
}
// 资源
// ResVirtualTableSetup 由 framework-v2 在 sqlite_vtable build tag 下注册到容器。
// resource.DB() 通过 do.Invoke 取出后调用,完成 vtab 模块注册和 CREATE VIRTUAL TABLE。
// res 提供 Code / Table / Fields 信息,vt 是 ResVirtualTable 的具体实现。
type ResVirtualTableSetup func(res Resource) error
// Resource 资源
type Resource struct {
container *do.Injector
container do.Injector
initOnce *sync.Once
Uuid string `db:"uuid"`
PUuid string `db:"pUuid"`
Code string `db:"code"`
Name string `db:"name"`
IsResSystem bool `db:"isSystem"`
IsResVirtual bool `db:"isVirtual"`
Table string `db:"table"`
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"`
Uuid string `db:"uuid"`
PUuid string `db:"pUuid"`
Code string `db:"code"`
Name string `db:"name"`
Conn string `db:"conn"`
IsResVirtual bool `db:"isVirtual"`
Table string `db:"table"`
Primarykey string `db:"primaryKey"`
HistoryRoles []string `db:"historyRoles"`
Fields []ResField `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) {
func (this *Resource) InitContainer(container do.Injector) {
this.container = container
this.initOnce = &sync.Once{}
}
// 资源UUID
// GetUuid 资源UUID
func (this Resource) GetUuid() string {
return this.Uuid
}
// 资源CODE
// GetCode 资源CODE
func (this Resource) GetCode() string {
return this.Code
}
// 资源名
// GetName 资源名
func (this Resource) GetName() string {
return this.Name
}
// 主键
// GetConn 数据库连接
func (this Resource) GetConn() string {
return this.Conn
}
// GetPrimarykey 主键
func (this Resource) GetPrimarykey() string {
return this.Primarykey
}
// 是否虚拟资源
// IsVirtual 是否虚拟资源
func (this Resource) IsVirtual() bool {
return this.IsResVirtual
}
// 是否系统资源
// IsSystem 是否系统资源
func (this Resource) IsSystem() bool {
return this.IsResSystem
return this.Conn == "service-support"
}
// 资源字段
// GetFields 资源字段
func (this Resource) GetFields() (result []req.ResField) {
for _, item := range this.Fields {
result = append(result, item)
@@ -99,137 +115,145 @@ func (this Resource) GetFields() (result []req.ResField) {
return result
}
// 资源字段
// GetField 资源字段
func (this Resource) GetField(code string) (req.ResField, bool) {
return lo.Find(this.GetFields(), func(v req.ResField) bool {
return v.GetCode() == code
})
}
// 判断资源字段
// HasField 判断资源字段
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()
// BeginTransaction 开启事物
func (this Resource) BeginTransaction() (*db.TxDatabase, error) {
return this.DB().Begin()
}
// 获取资源链接
func (this Resource) GetDBConn() *db.Connection {
db := do.MustInvoke[db.DB](this.container)
// DB 获取资源链接
func (this Resource) DB() *db.Database {
dbEngine := do.MustInvoke[engine.Engine](this.container)
conn := dbEngine.Connection(this.Conn)
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)
if isLocalDB(conn.Dialect()) && !this.IsResVirtual && this.Table != "" && this.initOnce != nil {
this.initOnce.Do(func() {
if err := this.autoCreateTable(conn); err != nil {
panic(err)
}
}
})
// 资源事件
this.onResEvent(builder)
// 用户事件
if this.IsHistoryRecord {
this.onUserEvent(builder, user)
})
}
// 虚拟资源暂时不考虑鉴权
if !this.IsVirtual() {
// 返回鉴权后的 DB Builder
// return
// vtable 资源:查询走 engine 中的 :memory: 连接("__"+Conn),
// 原文件连接仅用于 _vtab_cache 持久化,两者互不阻塞。
if conn.Dialect() == "vtable" && this.Table != "" && this.initOnce != nil {
memConn := dbEngine.Connection("__" + this.Conn)
this.initOnce.Do(func() {
if err := this.autoSetupVtab(memConn); err != nil {
panic(err)
}
})
return memConn
}
return builder.Table(string(this.GetTable()), alias)
return conn
}
func (this Resource) WithRolesCondition(b *db.Builder, t string, u req.User) error {
// autoSetupVtab 通过容器获取 ResVirtualTableSetup(由 framework-v2 在 sqlite_vtable tag 下注册),
// 再从 App 取出当前资源对应的 ResVirtualTable 实现,完成 vtab 注册和 CREATE VIRTUAL TABLE。
func (this Resource) autoSetupVtab(conn *db.Database) error {
_, err := conn.Exec(fmt.Sprintf(`CREATE VIRTUAL TABLE IF NOT EXISTS %s USING %s_mod()`, this.Table, this.Table))
return err
}
// autoCreateTable 自动创建数据表
func (this Resource) autoCreateTable(conn *db.Database) error {
sb := schema.New(conn)
return sb.Create(this.Table, func(bp *schema.Blueprint) {
bp.Comment = this.Name
for _, field := range this.Fields {
field.ToBlueprint(bp)
}
})
}
// GetTable 获取资源对应的数据表
func (this Resource) GetTable() exp.LiteralExpression {
if this.IsResVirtual {
return db.L("(" + this.Table + ")")
}
return db.V(db.T(this.Table))
}
// GetDBTable 获取资源对应的数据库连接
func (this Resource) GetDBTable(u req.User, opts ...req.ResOption) (sd *db.SelectDataset) {
if u == nil {
panic("GetDBTable: user cannot be nil")
}
op := &req.ResOptions{}
for _, cb := range opts {
cb(op)
}
alias := this.GetCode()
if op.Alias != "" {
alias = op.Alias
}
sd = this.DB().From(this.GetTable().As(alias))
if op.Tx != nil {
sd = op.Tx.From(this.GetTable().As(alias))
}
sd.WithHook(&ResourceHooks{res: this, u: u, tx: op.Tx})
return sd
}
func (this Resource) GetRolesCondition(u req.User) (*db.SelectDataset, db.Expression) {
isFullRight := false
isFullNot := false
if u == nil {
u = GetAnonymous()
}
// 如果是系统用户, 则不做校验
if u.Uuid() == "ffffffff-ffff-ffff-ffff-ffffffffffff" {
return nil, nil
}
// 这里不能放在 u.Uuid() == "ffffffff-ffff-ffff-ffff-ffffffffffff" 前面
// 会导致初始化数据时循环依赖
GetResRoles, err := do.Invoke[GetResRoles](this.container)
if err != nil {
return nil, nil
}
items := GetResRoles(this.GetUuid(), u.Roles()...)
if len(items) == 0 {
return nil, nil
}
GetResource := do.MustInvoke[contracts.GetResource](this.container)
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() + "`.*"))
sub := lo.Reduce(items, func(carry *db.SelectDataset, item ResRole, _ int) *db.SelectDataset {
sd := this.DB().From(this.GetTable().As(this.GetCode())).Select(db.T(this.GetCode()).All())
joins := lo.Filter(GetResRelations(item.Uuid), func(item ResRelation, _ int) bool {
return item.Type == "inner" || item.Type == "left" || item.Type == "right"
@@ -249,106 +273,52 @@ func (this Resource) WithRolesCondition(b *db.Builder, t string, u req.User) err
// 关联扩展条件
join.SetCondition(GetOrmConditions(joins[i].Uuid, condition.Describe("关联扩展条件")))
join.Inject(db, NewOrmModel(rResource, rResource.GetCode(), rResource.GetName()))
join.Inject(sd, NewOrmModel(rResource, rResource.GetCode(), rResource.GetName()))
}
conditions := GetOrmConditions(item.Uuid, condition.Describe("关联扩展条件"))
if len(joins) == 0 && conditions.IsEmpty() {
// 无权限, 直接跳过这个 unoin 语句
if carry != "" {
if carry != nil {
return carry
}
// 第一个无权限除外, 避免所有用户所属角色都是无权限
db.WhereRaw("false")
sd = sd.Where(db.V(false))
isFullNot = true
} else if len(joins) == 0 && conditions.IsNotEmpty() && conditions.IsAlwaysRight() /* 1=1 的这种条件*/ {
// 只要有1个满权限, 直接返回单条语句
isFullRight = true
return db.ToSql()
return sd
} else if conditions.IsNotEmpty() {
oOrm := NewOrm(this, nil)
oOrm.SetGlobalParams(req.NewGlobalParam("{}", u))
oOrm.SetGlobalParams(req.NewGlobalParams("{}", u))
db.Where(conditions.ToSql(oOrm.GetModel()))
sd = sd.Where(conditions.ToSql(oOrm.GetModel()))
// 如果前面是无权限的sql查看, 这直接返回本次查询
if isFullNot {
isFullNot = false
return db.ToSql()
return sd
}
}
if carry != "" {
carry += " UNION "
if carry != nil {
return carry.Union(sd)
}
return fmt.Sprintf("%s(%s)", carry, db.ToSql())
}, "")
return sd
}, nil)
if isFullRight {
return nil
return nil, 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, db.V(false)
}
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 {
}
return sub, nil
}
func (this Resource) GetStruct(extends ...reflect.StructField) any {
@@ -379,67 +349,66 @@ func (this Resource) GetSliceStruct(extends ...reflect.StructField) any {
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
}
// onResEvent 资源事件
func (this Resource) onResEvent(t string, result sql.Result) {
if num, err := result.RowsAffected(); num == 0 || err != nil {
return
}
// 有些地方可能没有注册 contracts.Queue
if qu, err := do.Invoke[contracts.Queue](this.container); err == nil {
// 全局触发器
// 1. 清除系统缓存
if err := do.MustInvoke[contracts.Queue](this.container).Publish(ResChangeEventTopic, ResChangeEventTopicPayload{
if err := qu.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{}
// onUserEvent 用户事件
func (this Resource) onUserEvent(builder exec.QueryExecutor, 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.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
}
// 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 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)
}
})
// // 触发消息队列
// 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 req.Resource, code, name, sql string, fields []ResField) req.Resource {
func NewVirtualResource(pRes req.Resource, code, name string, sd *db.SelectDataset, fields []ResField) req.Resource {
fieldsCopy := make([]ResField, len(fields))
copy(fieldsCopy, fields)
@@ -448,17 +417,21 @@ func NewVirtualResource(pRes req.Resource, code, name, sql string, fields []ResF
fieldsCopy[i].CodeResource = code
}
sql, _, err := sd.WithDialect(pRes.DB().Dialect()).ToSQL()
if err != nil {
panic(err)
}
return &Resource{
Uuid: code,
PUuid: pRes.GetUuid(),
Code: code,
Name: name,
IsResVirtual: true,
IsResSystem: pRes.IsSystem(),
Table: sql,
IsHistoryRecord: false,
HistoryCacheMax: 0,
Fields: fieldsCopy,
Roles: nil,
Uuid: code,
PUuid: pRes.GetUuid(),
Code: code,
Name: name,
IsResVirtual: true,
Conn: pRes.GetConn(),
Table: sql,
Fields: fieldsCopy,
Roles: nil,
}
}
+426
View File
@@ -0,0 +1,426 @@
package base
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"reflect"
"strconv"
"strings"
"sync"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/exp"
"git.fsdpf.net/go/db/schema"
"git.fsdpf.net/go/req"
"github.com/samber/lo"
)
var changeLogOnce sync.Map // key: Conn,每个数据库连接只建一次 _change_log 表
// isLocalDB 判断是否为本地文件型数据库(LastInsertId 返回最后一条而非第一条)
func isLocalDB(dialect string) bool {
return dialect == "sqlite3" || dialect == "duckdb"
}
type ResourceHooks struct {
res Resource
u req.User
tx *db.TxDatabase
snapshots []map[string]any // UPDATE/DELETE 前预查的行数据
}
func (rh *ResourceHooks) isTracked() bool {
if len(rh.res.HistoryRoles) == 0 || rh.res.IsVirtual() {
return false
}
if lo.Contains(rh.res.HistoryRoles, "00000000-0000-0000-0000-000000000000") {
return true
}
return lo.Some(rh.res.HistoryRoles, rh.u.Roles())
}
// autoCreateChangeLog 在当前连接上建 _change_log 表,每个连接只执行一次
func (rh *ResourceHooks) autoCreateChangeLog() error {
conn := rh.res.DB()
v, _ := changeLogOnce.LoadOrStore(rh.res.Conn, &sync.Once{})
var err error
v.(*sync.Once).Do(func() {
sb := schema.New(conn)
err = sb.Create("_change_log", func(bp *schema.Blueprint) {
bp.Comment = "资源数据变更记录"
bp.BigIncrements("id").AutoIncrement().Comment("ID")
bp.Boolean("enabled").Default("1").Comment("是否有效")
bp.Char("created_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("操作用户")
bp.Char("owned_user", 36).Default("00000000-0000-0000-0000-000000000000").Comment("拥有者")
bp.Timestamp("created_at").UseCurrent().Comment("操作时间")
bp.Timestamp("updated_at").UseCurrent().Default(db.L("ON UPDATE CURRENT_TIMESTAMP")).Comment("更新时间")
bp.DateTime("deleted_at").Nullable().Comment("删除时间")
bp.Char("resource_uuid", 36).Default("").Comment("资源UUID")
bp.String("category", 10).Default("").Comment("操作类型 INSERT/UPDATE/DELETE")
bp.Char("trace_id", 36).Default("").Comment("请求追踪ID")
bp.Integer("row_id").Default("0").Comment("受影响行ID")
bp.Json("snapshot").Nullable().Comment("操作前数据快照")
})
})
return err
}
// captureSnapshot 在写操作执行前,查出受影响的行存入 snapshots
func (rh *ResourceHooks) captureSnapshot(where exp.ExpressionList) {
sd := rh.res.DB().From(rh.res.GetTable())
if where != nil && len(where.Expressions()) > 0 {
sd = sd.Where(where.Expressions()...)
}
rows, err := sd.Executor().GetRecords()
if err != nil {
log.Printf("_change_log captureSnapshot err: %v", err)
return
}
rh.snapshots = rows
}
// writeChangeLogs 将变更写入 _change_log
func (rh *ResourceHooks) writeChangeLogs(category string, result sql.Result) {
if err := rh.autoCreateChangeLog(); err != nil {
log.Printf("_change_log init err: %v", err)
return
}
traceId := rh.u.Runtime().TraceId()
opUser := rh.u.Uuid()
var rows []any
switch category {
case "INSERT":
lastId, _ := result.LastInsertId()
count, _ := result.RowsAffected()
for i := int64(0); i < count; i++ {
rowId := lastId + i // MySQL: lastId 是第一条
if isLocalDB(rh.res.DB().Dialect()) {
rowId = lastId - count + 1 + i // SQLite: lastId 是最后一条
}
rows = append(rows, map[string]any{
"enabled": true,
"created_user": opUser,
"owned_user": opUser,
"resource_uuid": rh.res.Uuid,
"category": category,
"trace_id": traceId,
"row_id": rowId,
"snapshot": nil,
})
}
case "UPDATE", "DELETE":
for _, snap := range rh.snapshots {
rowId := int64(0)
if id, ok := snap["id"]; ok {
rowId = toChangeLogInt64(id)
}
snapJSON, _ := json.Marshal(snap)
rows = append(rows, map[string]any{
"enabled": true,
"created_user": opUser,
"owned_user": opUser,
"resource_uuid": rh.res.Uuid,
"category": category,
"trace_id": traceId,
"row_id": rowId,
"snapshot": string(snapJSON),
})
}
}
if len(rows) == 0 {
return
}
inserter := rh.res.DB().Insert("_change_log")
if rh.tx != nil {
inserter = rh.tx.Insert("_change_log")
}
if _, err := inserter.Rows(rows...).Executor().Exec(); err != nil {
log.Printf("_change_log write err: %v", err)
}
}
func toChangeLogInt64(v any) int64 {
switch val := v.(type) {
case int64:
return val
case int:
return int64(val)
case int32:
return int64(val)
case float64:
return int64(val)
case string:
n, _ := strconv.ParseInt(val, 10, 64)
return n
}
return 0
}
func (rh *ResourceHooks) Before(dataset interface{}) error {
switch d := dataset.(type) {
case *db.SelectDataset:
return rh.beforeSelectDataset(d)
case *db.InsertDataset:
return rh.beforeInsertDataset(d)
case *db.UpdateDataset:
return rh.beforeUpdateDataset(d)
case *db.DeleteDataset:
return rh.beforeDeleteDataset(d)
}
return nil
}
func (rh *ResourceHooks) After(dataset interface{}, result interface{}) error {
if rh.res.IsVirtual() {
// 返回鉴权后的 DB Builder
return nil
}
// 用户事件
// if rh.res.IsHistoryRecord {
// rh.res.onUserEvent(builder, user)
// }
switch dataset.(type) {
case *db.SelectDataset:
case *db.InsertDataset:
r := result.(sql.Result)
rh.res.onResEvent("INSERT", r)
if rh.isTracked() {
rh.writeChangeLogs("INSERT", r)
}
case *db.UpdateDataset:
r := result.(sql.Result)
rh.res.onResEvent("UPDATE", r)
if rh.isTracked() {
rh.writeChangeLogs("UPDATE", r)
}
case *db.DeleteDataset:
r := result.(sql.Result)
rh.res.onResEvent("DELETE", r)
if rh.isTracked() {
rh.writeChangeLogs("DELETE", r)
}
}
return nil
}
func (rh *ResourceHooks) beforeInsertDataset(id *db.InsertDataset) error {
switch true {
case id.GetClauses().HasRows():
return rh.beforeInsertRows(id)
case id.GetClauses().HasVals():
return rh.beforeInsertColsVals(id)
case id.GetClauses().HasFrom():
return rh.beforeInsertFromQuery(id)
}
return nil
}
func (rh *ResourceHooks) beforeInsertRows(id *db.InsertDataset) error {
rows := id.GetClauses().Rows()
for i := range rows {
rowValue := reflect.ValueOf(rows[i])
if rowValue.Kind() == reflect.Ptr {
rowValue = rowValue.Elem()
}
if rowValue.Kind() == reflect.Struct {
if row, err := exp.NewRecordFromStruct(rowValue.Interface(), true, false); err != nil {
return err
} else {
rows[i] = row
}
}
}
for i := range rows {
row := rows[i].(map[string]any)
// 格式化保存数据
if err := rh.normalizeSaveValue(row); err != nil {
return err
}
// 填充默认数据
if err := rh.applyDefaultValue(row, true, false); err != nil {
return err
}
}
return nil
}
func (rh *ResourceHooks) beforeInsertColsVals(id *db.InsertDataset) error {
cols := id.GetClauses().Cols()
vals := id.GetClauses().Vals()
colsName := []string{}
for _, col := range cols.Columns() {
colsName = append(colsName, col.(exp.IdentifierExpression).GetCol().(string))
}
colsVal := []any{}
for k, v := range lo.OmitByKeys(rh.getFieldsDefaultValue(true, false), colsName) {
cols = cols.Append(db.C(k))
colsVal = append(colsVal, v)
}
for i := 0; i < len(vals); i++ {
for j := 0; j < len(vals[i]); j++ {
if filed, ok := rh.res.GetField(colsName[j]); ok {
vals[i][j] = filed.ToValue(vals[i][j])
}
}
vals[i] = append(vals[i], colsVal...)
}
*id = *id.Cols(cols)
return nil
}
func (rh *ResourceHooks) beforeInsertFromQuery(id *db.InsertDataset) error {
return nil
}
// 规范化保存数据
// 1. 移除系统缺省字段, 如果是 insert 需要调用 applyDefaultValue
// 2. 移除系统中没有的字段
func (rh *ResourceHooks) normalizeSaveValue(row db.Record) error {
for k, v := range row {
if k == "id" || k == "created_user" || k == "created_at" || k == "deleted_at" || k == "updated_at" {
delete(row, k)
} else if val, ok := v.(db.Expression); ok {
row[k] = val
} else if field, ok := rh.res.GetField(k); ok {
row[k] = field.ToValue(v)
} else {
delete(row, k)
}
}
return nil
}
// 填充默认数据
func (rh *ResourceHooks) applyDefaultValue(row db.Record, forInsert, forUpdate bool) error {
// 填充默认数据
for k, v := range rh.getFieldsDefaultValue(forInsert, forUpdate) {
if _, ok := row[k]; !ok {
row[k] = v
}
}
return nil
}
func (rh *ResourceHooks) getFieldsDefaultValue(forInsert, forUpdate bool) map[string]db.Expression {
defs := map[string]db.Expression{}
if forInsert {
for _, item := range rh.res.Fields {
if item.GetCode() == "updated_at" || item.GetCode() == "created_user" || item.GetCode() == "owned_user" {
continue
}
if len(item.Default) > 4 && strings.ToLower(item.Default[0:4]) == "sql:" {
defs[item.GetCode()] = item.GetRawDefault()
} else if item.DataType == req.ResJson {
defs[item.GetCode()] = item.GetRawDefault()
}
}
if _, ok := defs["owned_user"]; !ok {
defs["owned_user"] = db.V(rh.u.Uuid())
}
defs["created_user"] = db.V(rh.u.Uuid())
}
if forUpdate && isLocalDB(rh.res.DB().Dialect()) {
defs["updated_at"] = db.L("CURRENT_TIMESTAMP")
}
return defs
}
func (rh *ResourceHooks) beforeSelectDataset(sd *db.SelectDataset) error {
sub, ex := rh.res.GetRolesCondition(rh.u)
if ex != nil {
*sd = *sd.Where(ex)
} else if sub != nil {
table := sd.GetClauses().From().Columns()[0]
if alias, ok := table.(exp.AliasedExpression); ok {
*sd = *sd.From(db.V(sub.Expression()).As(alias.GetAs()))
} else {
*sd = *sd.From(db.V(sub.Expression()).As(table))
}
}
return nil
}
func (rh *ResourceHooks) beforeUpdateDataset(ud *db.UpdateDataset) error {
// 虚拟资源暂时不考虑鉴权
if rh.res.IsVirtual() {
return nil
}
sub, ex := rh.res.GetRolesCondition(rh.u)
if ex != nil {
*ud = *ud.Where(ex)
} else if sub != nil {
table := ud.GetClauses().Table()
if alias, ok := table.(exp.AliasedExpression); ok {
*ud = *ud.Where(alias.GetAs().Col("id").In(sub.Select(db.T(rh.res.GetCode()).Col("id"))))
} else {
*ud = *ud.Where(db.L("?.id", table).In(sub.Select(db.T(rh.res.GetCode()).Col("id"))))
}
}
// 记录变更前快照(role 条件已应用,WHERE 与实际 UPDATE 一致)
if rh.isTracked() {
rh.captureSnapshot(ud.GetClauses().Where())
}
// 格式化保存数据
if ud.GetClauses().HasSetValues() {
udv := ud.GetClauses().SetValues()
switch data := udv.(type) {
case map[string]any:
return rh.normalizeSaveValue(data)
case db.Record:
return rh.normalizeSaveValue(data)
default:
return fmt.Errorf("unsupported type: %T", udv)
}
}
return nil
}
func (rh *ResourceHooks) beforeDeleteDataset(dd *db.DeleteDataset) error {
// 虚拟资源暂时不考虑鉴权
if rh.res.IsVirtual() {
return nil
}
sub, ex := rh.res.GetRolesCondition(rh.u)
if ex != nil {
*dd = *dd.Where(ex)
} else if sub != nil {
table := dd.GetClauses().From().Columns()[0]
if alias, ok := table.(exp.AliasedExpression); ok {
*dd = *dd.Where(alias.GetAs().Col("id").In(sub.Select(db.T(rh.res.GetCode()).Col("id"))))
} else {
*dd = *dd.Where(db.L("?.id", table).In(sub.Select(db.T(rh.res.GetCode()).Col("id"))))
}
}
// 记录变更前快照(role 条件已应用,WHERE 与实际 DELETE 一致)
if rh.isTracked() {
rh.captureSnapshot(dd.GetClauses().Where())
}
return nil
}
+100
View File
@@ -0,0 +1,100 @@
package base_test
import (
"os"
"testing"
"git.fsdpf.net/go/contracts/base"
"git.fsdpf.net/go/db"
"git.fsdpf.net/go/db/engine"
"git.fsdpf.net/go/req"
"github.com/samber/do/v2"
"github.com/stretchr/testify/suite"
_ "git.fsdpf.net/go/db/schema/dialect/mysql"
_ "git.fsdpf.net/go/db/schema/dialect/sqlite3"
)
type resourceTest struct {
suite.Suite
res *base.Resource
}
func TestResource(t *testing.T) {
suite.Run(t, new(resourceTest))
}
func (t *resourceTest) SetupSuite() {
app := do.New()
do.Provide(app, func(container do.Injector) (engine.Engine, error) {
return engine.Open(map[string]engine.DBConfig{
"default": engine.NewDBConfig("mysql",
engine.WithHost(os.Getenv("MYSQL_HOST")),
engine.WithPort(os.Getenv("MYSQL_PORT")),
engine.WithDatabase(os.Getenv("MYSQL_DB")),
engine.WithUsername(os.Getenv("MYSQL_USER")),
engine.WithPassword(os.Getenv("MYSQL_PASSWD")),
),
}), nil
})
fields := []base.ResField{
{Name: "字段", Code: "fields", DataType: req.ResJson, Default: "{}"},
}
res := &base.Resource{
Uuid: "4bf3a311-cbe1-4236-bdda-c64ab04ae9b1",
Code: "User",
Name: "用户",
Table: "users",
Primarykey: "id",
Fields: fields,
}
res.InitContainer(app)
t.res = res
}
func (t *resourceTest) TestInsertRows() {
type Temp struct {
Name string `db:"name"`
Fields map[string]any `db:"fields"`
}
result := "INSERT INTO `users` AS `User` (`created_user`, `fields`, `name`, `owned_user`) " +
"VALUES ('00000000-0000-0000-0000-000000000000', '{\\\"aa\\\":1}', '张三', '00000000-0000-0000-0000-000000000000')"
sql, _, _ := t.res.GetDBTable(base.GetAnonymous()).Insert().Rows(
db.Record{"name": "张三", "id": 1, "fields": map[string]any{"aa": 1}},
).Executor().ToSQL()
t.Equal(result, sql)
sql, _, _ = t.res.GetDBTable(base.GetAnonymous()).Insert().Rows(
&Temp{Name: "张三", Fields: map[string]any{"aa": 1}},
).Executor().ToSQL()
t.Equal(result, sql)
sql, _, _ = t.res.GetDBTable(base.GetAnonymous()).Insert().Rows(
Temp{Name: "张三", Fields: map[string]any{"aa": 1}},
).Executor().ToSQL()
t.Equal(result, sql)
}
func (t *resourceTest) TestInsertColsVals() {
result := "INSERT INTO `users` AS `User` (`name`, `fields`, `created_user`, `owned_user`) VALUES " +
"('李四', '{\\\"aa\\\":1}', '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000'), " +
"('王五', '{\\\"aa\\\":3}', '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000')"
sql, _, _ := t.res.GetDBTable(base.GetAnonymous()).Insert().Cols("name", "fields").Vals(
db.Vals{"李四", map[string]any{"aa": 1}},
db.Vals{"王五", map[string]any{"aa": 3}},
).Executor().ToSQL()
_ = result
_ = sql
// t.Equal(result, sql)
}
func (*resourceTest) TestInsertColsFromQuery() {}
+1
View File
@@ -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"`
}
+2 -2
View File
@@ -51,7 +51,7 @@ func (this user) Runtime() req.UserRuntime {
return this.runtime
}
// 获取匿名用户
// GetAnonymous 获取匿名用户
func GetAnonymous(opts ...req.UserRuntimeOption) req.User {
u := user{
id: 0,
@@ -69,7 +69,7 @@ func GetAnonymous(opts ...req.UserRuntimeOption) req.User {
return u
}
// 系统用户
// GetSystemUser 系统用户
func GetSystemUser(opts ...req.UserRuntimeOption) req.User {
u := user{
id: -1,
+23
View File
@@ -0,0 +1,23 @@
package base
import (
"git.fsdpf.net/go/req"
)
type UserAsset struct {
ID int64 `db:"id"`
Name string `db:"name"`
FileName string `db:"filename"`
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)