Files
contracts/base/resource.go
T
2026-05-15 09:04:12 +08:00

438 lines
12 KiB
Go

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,
}
}