This commit is contained in:
孟帅
2024-04-22 23:08:40 +08:00
parent 82483bd7b9
commit e144b12580
445 changed files with 17457 additions and 6708 deletions

View File

@@ -38,6 +38,39 @@ func GenJoinOnRelation(masterTable, masterField, joinTable, alias, onField strin
return []string{joinTable, alias, relation}
}
func JoinFields(ctx context.Context, entity interface{}, dao daoInstance, as string) (fs string) {
entityFs, err := convert.GetEntityFieldTags(entity)
if err != nil {
return
}
if len(entityFs) == 0 {
return ""
}
fields, err := dao.Ctx(ctx).TableFields(dao.Table())
if err != nil {
return
}
var columns []string
for _, v := range entityFs {
if !gstr.HasPrefix(v, as) {
continue
}
field := gstr.CaseSnakeFirstUpper(gstr.StrEx(v, as))
if _, ok := fields[field]; ok {
columns = append(columns, fmt.Sprintf("`%s`.`%s` as `%s`", dao.Table(), field, v))
}
}
if len(columns) > 0 {
return gstr.Implode(",", convert.UniqueSlice(columns))
}
return
}
// GenJoinSelect 生成关联表select
// 这里会将实体中的字段驼峰转为下划线于数据库进行匹配,意味着数据库字段必须全部是小写字母+下划线的格式
func GenJoinSelect(ctx context.Context, entity interface{}, dao daoInstance, joins []*Join) (allFields string, err error) {

View File

@@ -0,0 +1,92 @@
// Package hgorm
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package hgorm
import (
"context"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"hotgo/internal/consts"
"hotgo/utility/tree"
)
// TenantRelation 租户关系
type TenantRelation struct {
DeptType string // 部门类型
TenantId int64 // 租户ID
MerchantId int64 // 商户ID
UserId int64 // 用户ID
}
// GetTenantRelation 获取租户关系
func GetTenantRelation(ctx context.Context, memberId int64) (tr *TenantRelation, err error) {
data, err := g.Model("hg_admin_member u").Ctx(ctx).
LeftJoin("hg_admin_dept d", "u.dept_id=d.id").
Fields("u.tree,d.type").
Where("u.id", memberId).One()
if err != nil {
return nil, err
}
if data.IsEmpty() {
err = gerror.Newf("未找到用户[%v]的租户关系,该用户不存在", memberId)
return
}
ids := tree.GetIds(data["tree"].String())
getRelationId := func(deptType string) (int64, error) {
id, err := g.Model("hg_admin_member u").Ctx(ctx).
LeftJoin("hg_admin_dept d", "u.dept_id=d.id").
Fields("u.id").
WhereIn("u.id", ids).Where("d.type", deptType).
OrderAsc("u.level"). // 确保是第一关系
Limit(1).
Value()
if err != nil {
return 0, err
}
if id.Int64() < 1 {
err = gerror.Newf("未找到有效的租户关系,memberId:%v,deptType:%v", memberId, deptType)
return 0, err
}
return id.Int64(), nil
}
tr = new(TenantRelation)
tr.DeptType = data["type"].String()
switch tr.DeptType {
// 公司
case consts.DeptTypeCompany:
return
// 租户
case consts.DeptTypeTenant:
tr.TenantId = memberId
// 商户
case consts.DeptTypeMerchant:
tr.TenantId, err = getRelationId(consts.DeptTypeTenant)
if err != nil {
return nil, err
}
tr.MerchantId = memberId
// 用户
case consts.DeptTypeUser:
tr.TenantId, err = getRelationId(consts.DeptTypeTenant)
if err != nil {
return nil, err
}
tr.MerchantId, err = getRelationId(consts.DeptTypeMerchant)
if err != nil {
return nil, err
}
tr.UserId = memberId
default:
err = gerror.Newf("未找到用户[%]的租户关系,部门类型[%v] 无效", memberId, tr.DeptType)
return nil, err
}
return
}

View File

@@ -13,6 +13,7 @@ import (
"hotgo/internal/consts"
"hotgo/internal/library/contexts"
"hotgo/internal/model/entity"
"hotgo/utility/convert"
"hotgo/utility/tree"
)
@@ -22,7 +23,7 @@ func FilterAuth(m *gdb.Model) *gdb.Model {
var (
needAuth bool
filterField string
fields = escapeFieldsToSlice(m.GetFieldsStr())
fields = convert.EscapeFieldsToSlice(m.GetFieldsStr())
)
// 优先级created_by > member_id
@@ -99,11 +100,6 @@ func FilterAuthWithField(filterField string) func(m *gdb.Model) *gdb.Model {
}
}
// escapeFieldsToSlice 将转义过的字段转换为字段集切片
func escapeFieldsToSlice(s string) []string {
return gstr.Explode(",", gstr.Replace(gstr.Replace(s, "`,`", ","), "`", ""))
}
// GetDeptAndSub 获取指定部门的所有下级,含本部门
func GetDeptAndSub(ctx context.Context, deptId int64) (ids []int64) {
array, err := g.Model("admin_dept").

View File

@@ -3,7 +3,6 @@
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package handler
// handler.
@@ -13,8 +12,9 @@ import (
// Option 预处理选项
type Option struct {
FilterAuth bool // 过滤权限
ForceCache bool // 强制缓存
FilterAuth bool // 过滤权限
ForceCache bool // 强制缓存
FilterTenant bool // 过滤多租户权限
}
// DefaultOption 默认预处理选项
@@ -35,5 +35,8 @@ func Model(m *gdb.Model, opt ...*Option) *gdb.Model {
if option.ForceCache {
m = m.Handler(ForceCache)
}
if option.FilterTenant {
m = m.Handler(FilterTenant)
}
return m
}

View File

@@ -0,0 +1,49 @@
// Package handler
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package handler
import (
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/text/gstr"
"hotgo/internal/library/contexts"
"hotgo/utility/convert"
)
// FilterTenant 过滤多租户数据权限
// 根据部门类型识别当前租户、商户、用户身份,过滤只属于自己的数据
func FilterTenant(m *gdb.Model) *gdb.Model {
var (
needAuth bool
filterField string
fields = convert.EscapeFieldsToSlice(m.GetFieldsStr())
ctx = m.GetCtx()
)
// 租户
if contexts.IsTenantDept(ctx) && gstr.InArray(fields, "tenant_id") {
needAuth = true
filterField = "tenant_id"
}
// 商户
if contexts.IsMerchantDept(ctx) && gstr.InArray(fields, "merchant_id") {
needAuth = true
filterField = "merchant_id"
}
// 用户
if contexts.IsUserDept(ctx) && gstr.InArray(fields, "user_id") {
needAuth = true
filterField = "user_id"
}
if !needAuth {
return m
}
m = m.Where(filterField, contexts.GetUserId(ctx))
return m
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"hotgo/utility/convert"
)
// MemberInfo 后台用户信息
@@ -61,3 +62,76 @@ var MemberInfo = gdb.HookHandler{
return
},
}
type MemberSumma struct {
Id int64 `json:"id" description:"管理员ID"`
RealName string `json:"realName" description:"真实姓名"`
Username string `json:"username" description:"帐号"`
Avatar string `json:"avatar" description:"头像"`
}
// MemberSummary 操作人摘要信息
var MemberSummary = gdb.HookHandler{
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
result, err = in.Next(ctx)
if err != nil {
return
}
var (
createdByIds []int64
updatedByIds []int64
memberIds []int64
)
for _, record := range result {
if record["created_by"].Int64() > 0 {
createdByIds = append(createdByIds, record["created_by"].Int64())
}
if record["updated_by"].Int64() > 0 {
updatedByIds = append(updatedByIds, record["updated_by"].Int64())
}
if record["member_id"].Int64() > 0 {
memberIds = append(memberIds, record["member_id"].Int64())
}
}
memberIds = append(memberIds, createdByIds...)
memberIds = append(memberIds, updatedByIds...)
memberIds = convert.UniqueSlice(memberIds)
if len(memberIds) == 0 {
return
}
var members []*MemberSumma
if err = g.Model("admin_member").Ctx(ctx).WhereIn("id", memberIds).Scan(&members); err != nil {
return nil, err
}
if len(members) == 0 {
return
}
findMember := func(id *gvar.Var) *MemberSumma {
for _, v := range members {
if v.Id == id.Int64() {
return v
}
}
return nil
}
for _, record := range result {
if record["created_by"].Int64() > 0 {
record["createdBySumma"] = gvar.New(findMember(record["created_by"]))
}
if record["updated_by"].Int64() > 0 {
record["updatedBySumma"] = gvar.New(findMember(record["updated_by"]))
}
if record["member_id"].Int64() > 0 {
record["memberBySumma"] = gvar.New(findMember(record["member_id"]))
}
}
return
},
}

View File

@@ -0,0 +1,288 @@
// Package hook
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package hook
import (
"context"
"database/sql"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"hotgo/internal/consts"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm"
"hotgo/utility/convert"
)
// SaveTenant 自动维护更新租户关系字段
// 根据部门类型识别当前租户、商户、用户身份
var SaveTenant = gdb.HookHandler{
Insert: func(ctx context.Context, in *gdb.HookInsertInput) (result sql.Result, err error) {
h, err := newHookSaveTenant(ctx, in)
if err != nil {
return nil, err
}
return h.handle()
},
Update: func(ctx context.Context, in *gdb.HookUpdateInput) (result sql.Result, err error) {
h, err := newHookSaveTenant(ctx, in)
if err != nil {
return nil, err
}
return h.handle()
},
}
type hookSaveTenant struct {
ctx context.Context
in any
isNewRecord bool
relations map[int64]*hgorm.TenantRelation
}
func newHookSaveTenant(ctx context.Context, in any) (*hookSaveTenant, error) {
h := new(hookSaveTenant)
h.ctx = ctx
h.in = in
_, h.isNewRecord = in.(*gdb.HookInsertInput)
h.relations = make(map[int64]*hgorm.TenantRelation)
return h, nil
}
// getFields 获取表字段
func (h *hookSaveTenant) getFields() []string {
if h.isNewRecord {
in := h.in.(*gdb.HookInsertInput)
return convert.EscapeFieldsToSlice(in.Model.GetFieldsStr())
}
in := h.in.(*gdb.HookUpdateInput)
return convert.EscapeFieldsToSlice(in.Model.GetFieldsStr())
}
// getRelation 获取指定用户的租户关系
func (h *hookSaveTenant) getRelation(id int64) (tr *hgorm.TenantRelation, err error) {
v, ok := h.relations[id]
if ok {
return v, nil
}
h.relations[id], err = hgorm.GetTenantRelation(h.ctx, id)
if err != nil {
return nil, err
}
return h.relations[id], nil
}
// getData 获取更新数据
func (h *hookSaveTenant) getData() any {
if h.isNewRecord {
in := h.in.(*gdb.HookInsertInput)
return in.Data
}
in := h.in.(*gdb.HookUpdateInput)
return in.Data
}
// setData 修改更新数据
func (h *hookSaveTenant) setData(data any) {
if h.isNewRecord {
in := h.in.(*gdb.HookInsertInput)
in.Data = data.(gdb.List)
return
}
in := h.in.(*gdb.HookUpdateInput)
in.Data = data
}
func (h *hookSaveTenant) next() (result sql.Result, err error) {
if h.isNewRecord {
in := h.in.(*gdb.HookInsertInput)
return in.Next(h.ctx)
}
in := h.in.(*gdb.HookUpdateInput)
return in.Next(h.ctx)
}
// checkRelationConsistent 检查关系是否一致
func (h *hookSaveTenant) checkRelationConsistent(tid, mid, uid any) error {
var (
tenantId = gconv.Int64(tid)
merchantId = gconv.Int64(mid)
userId = gconv.Int64(uid)
)
// 存在用户,优先用用户开始检查
if userId > 0 {
tr, err := h.getRelation(userId)
if err != nil {
return err
}
if tenantId > 0 && tr.TenantId != tenantId {
err = gerror.Newf("租户[%v]与用户[%v]关系不匹配", tenantId, userId)
return err
}
if merchantId > 0 && tr.MerchantId != merchantId {
err = gerror.Newf("商户[%v]与用户[%v]关系不匹配", merchantId, userId)
return err
}
return nil
}
if merchantId > 0 {
tr, err := h.getRelation(merchantId)
if err != nil {
return err
}
if tenantId > 0 && tr.TenantId != tenantId {
err = gerror.Newf("租户[%v]与商户[%v]关系不匹配", tenantId, userId)
return err
}
return nil
}
return nil
}
// checkRelationSingle 检查单个用户关系
func (h *hookSaveTenant) checkRelationSingle(idx any, relation, limitType string) (err error) {
id := gconv.Int64(idx)
if id < 1 {
return
}
ok := false
tr, err := h.getRelation(id)
if err != nil {
return err
}
if tr.DeptType != limitType {
err = gerror.Newf("用户[%v]关系验证不通过,类型身份不匹配[%v != %v]", id, tr.DeptType, limitType)
return
}
relationId := contexts.GetUserId(h.ctx)
switch relation {
case consts.DeptTypeTenant:
if ok = tr.TenantId == relationId; !ok {
err = gerror.Newf("%v的租户不是%v", id, relationId)
return
}
case consts.DeptTypeMerchant:
if ok = tr.MerchantId == relationId; !ok {
err = gerror.Newf("%v的商户不是%v", id, relationId)
return
}
}
return
}
// 检查用户关系是否有效
func (h *hookSaveTenant) checkRelation(deptType string, data gdb.Map) (err error) {
switch deptType {
// 公司和用户,检查关系是否一致
case consts.DeptTypeCompany, consts.DeptTypeUser:
if err = h.checkRelationConsistent(data[consts.TenantId], data[consts.MerchantId], data[consts.UserId]); err != nil {
return
}
// 租户,检查商户和用户是否属于自己
case consts.DeptTypeTenant:
if err = h.checkRelationSingle(data[consts.MerchantId], consts.DeptTypeTenant, consts.DeptTypeMerchant); err != nil {
return
}
if err = h.checkRelationSingle(data[consts.UserId], consts.DeptTypeTenant, consts.DeptTypeUser); err != nil {
return
}
// 商户,检查用户是否属于自己
case consts.DeptTypeMerchant:
if err = h.checkRelationSingle(data[consts.UserId], consts.DeptTypeMerchant, consts.DeptTypeUser); err != nil {
return
}
}
return
}
func (h *hookSaveTenant) handle() (result sql.Result, err error) {
var (
update = make(g.Map)
fields = h.getFields()
memberId = contexts.GetUserId(h.ctx)
deptType = contexts.GetDeptType(h.ctx)
tr *hgorm.TenantRelation
)
if memberId == 0 || len(deptType) == 0 {
err = gerror.New("缺少用户上下文数据")
return nil, err
}
// 非公司类型,加载自己的租户关系,用于重写关系
if !contexts.IsCompanyDept(h.ctx) {
tr, err = h.getRelation(memberId)
if err != nil {
return nil, err
}
}
switch deptType {
// 公司
case consts.DeptTypeCompany:
// 租户
case consts.DeptTypeTenant:
if gstr.InArray(fields, consts.TenantId) {
update[consts.TenantId] = tr.TenantId
}
// 商户
case consts.DeptTypeMerchant:
if gstr.InArray(fields, consts.TenantId) {
update[consts.TenantId] = tr.TenantId
}
if gstr.InArray(fields, consts.MerchantId) {
update[consts.MerchantId] = tr.MerchantId
}
// 用户
case consts.DeptTypeUser:
if gstr.InArray(fields, consts.TenantId) {
update[consts.TenantId] = tr.TenantId
}
if gstr.InArray(fields, consts.MerchantId) {
update[consts.MerchantId] = tr.MerchantId
}
if gstr.InArray(fields, consts.UserId) {
update[consts.UserId] = tr.UserId
}
default:
err = gerror.Newf("当前用户部门类型[%v] 找到有效的hook请检查", deptType)
return nil, err
}
switch value := h.getData().(type) {
case gdb.List:
for i, data := range value {
if err = h.checkRelation(deptType, data); err != nil {
return nil, err
}
for k, v := range update {
data[k] = v
}
value[i] = data
}
h.setData(value)
case gdb.Map:
if err = h.checkRelation(deptType, value); err != nil {
return nil, err
}
for k, v := range update {
value[k] = v
}
h.setData(value)
}
return h.next()
}

View File

@@ -0,0 +1,67 @@
// Package hook
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package hook_test
import (
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/util/grand"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model"
"testing"
)
func init() {
g.DB().SetDryRun(true)
g.DB().SetDebug(true)
}
// TestSaveTenant_User 以用户身份增改购买订单数据
func TestSaveTenant_User(t *testing.T) {
// 设置上下文用户身份为用户
ctx := context.WithValue(gctx.New(), consts.ContextHTTPKey, &model.Context{
// 为了测试只设置了hook中需要用到的数据
User: &model.Identity{
Id: 12,
DeptType: consts.DeptTypeUser,
},
})
cols := dao.AddonHgexampleTenantOrder.Columns()
orderSn := grand.Letters(32)
// 以用户身份插入购买订单数据,自动维护租户关系字段
data := g.Map{
//cols.TenantId: 8,
//cols.MerchantId: 11,
//cols.UserId: 12,
cols.ProductName: "无线运动耳机",
cols.OrderSn: orderSn,
cols.Money: 99,
cols.Status: consts.PayStatusWait,
}
_, err := dao.AddonHgexampleTenantOrder.Ctx(ctx).Data(data).Hook(hook.SaveTenant).Insert()
if err != nil {
panic(err)
}
// 以用户身份插入修改订单数据,自动维护租户关系字段
update := g.Map{
cols.Status: consts.PayStatusOk,
}
_, err = dao.AddonHgexampleTenantOrder.Ctx(ctx).Where(cols.OrderSn, orderSn).Data(update).Hook(hook.SaveTenant).Update()
if err != nil {
panic(err)
}
}