contracts/base/resource.go

465 lines
12 KiB
Go

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"`
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"`
}
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.IsResSystem
}
// 资源字段
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 req.Resource, code, name, sql string, fields []ResField) req.Resource {
fieldsCopy := make([]ResField, len(fields))
copy(fieldsCopy, fields)
for i := 0; i < len(fieldsCopy); i++ {
fieldsCopy[i].CodeResource = code
}
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,
}
}