package base import ( "database/sql" "fmt" "log" "reflect" "sync" "unicode" "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 { 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 // 新数据 } // 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 initOnce *sync.Once 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) { this.container = container this.initOnce = &sync.Once{} } // GetUuid 资源UUID func (this Resource) GetUuid() string { return this.Uuid } // 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.Conn == "service-support" } // GetFields 资源字段 func (this Resource) GetFields() (result []req.ResField) { for _, item := range this.Fields { result = append(result, item) } 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 }) } // BeginTransaction 开启事物 func (this Resource) BeginTransaction() (*db.TxDatabase, error) { return this.DB().Begin() } // DB 获取资源链接 func (this Resource) DB() *db.Database { dbEngine := do.MustInvoke[engine.Engine](this.container) conn := dbEngine.Connection(this.Conn) if isLocalDB(conn.Dialect()) && !this.IsResVirtual && this.Table != "" && this.initOnce != nil { this.initOnce.Do(func() { if err := this.autoCreateTable(conn); err != nil { panic(err) } }) } // 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 conn } // 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) GetOrmConditions := do.MustInvoke[contracts.GetOrmConditions](this.container) 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" }) 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(sd, NewOrmModel(rResource, rResource.GetCode(), rResource.GetName())) } conditions := GetOrmConditions(item.Uuid, condition.Describe("关联扩展条件")) if len(joins) == 0 && conditions.IsEmpty() { // 无权限, 直接跳过这个 unoin 语句 if carry != nil { return carry } // 第一个无权限除外, 避免所有用户所属角色都是无权限 sd = sd.Where(db.V(false)) isFullNot = true } else if len(joins) == 0 && conditions.IsNotEmpty() && conditions.IsAlwaysRight() /* 1=1 的这种条件*/ { // 只要有1个满权限, 直接返回单条语句 isFullRight = true return sd } else if conditions.IsNotEmpty() { oOrm := NewOrm(this, nil) oOrm.SetGlobalParams(req.NewGlobalParams("{}", u)) sd = sd.Where(conditions.ToSql(oOrm.GetModel())) // 如果前面是无权限的sql查看, 这直接返回本次查询 if isFullNot { isFullNot = false return sd } } if carry != nil { return carry.Union(sd) } return sd }, nil) if isFullRight { return nil, nil } if isFullNot { return nil, db.V(false) } return sub, nil } 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() } // 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 := qu.Publish(ResChangeEventTopic, ResChangeEventTopicPayload{ Type: t, Res: this, Result: result, }); err != nil { log.Println("Queue Publish Err:", ResChangeEventTopic, err) } } } // 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.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 req.Resource, code, name string, sd *db.SelectDataset, fields []ResField) req.Resource { fieldsCopy := make([]ResField, len(fields)) copy(fieldsCopy, fields) for i := 0; i < len(fieldsCopy); i++ { 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, Conn: pRes.GetConn(), Table: sql, Fields: fieldsCopy, Roles: nil, } }