From 4722eef72ccf22cbc39036f8f18c944b786558a8 Mon Sep 17 00:00:00 2001 From: Rogee Date: Tue, 16 Dec 2025 14:14:58 +0800 Subject: [PATCH] feat: add user_tenant associations --- backend/app/http/super/provider.gen.go | 1 + backend/app/services/user.go | 2 +- backend/app/services/user_test.go | 60 ++++++ backend/database/.transform.yaml | 20 ++ backend/database/models/tenants.gen.go | 1 + backend/database/models/tenants.query.gen.go | 93 ++++++++- backend/database/models/users.gen.go | 22 +- backend/database/models/users.query.gen.go | 207 +++++++++++++++++-- backend/pkg/utils/json.go | 9 + 9 files changed, 391 insertions(+), 24 deletions(-) create mode 100644 backend/pkg/utils/json.go diff --git a/backend/app/http/super/provider.gen.go b/backend/app/http/super/provider.gen.go index 258145f..957d649 100755 --- a/backend/app/http/super/provider.gen.go +++ b/backend/app/http/super/provider.gen.go @@ -2,6 +2,7 @@ package super import ( "database/sql" + "quyun/v2/providers/app" "go.ipao.vip/atom" diff --git a/backend/app/services/user.go b/backend/app/services/user.go index 08517c1..20dd489 100644 --- a/backend/app/services/user.go +++ b/backend/app/services/user.go @@ -16,7 +16,7 @@ type user struct{} func (t *user) FindByID(ctx context.Context, userID int64) (*models.User, error) { tbl, query := models.UserQuery.QueryContext(ctx) - model, err := query.Where(tbl.ID.Eq(userID)).First() + model, err := query.Preload(tbl.OwnedTenant, tbl.Tenants).Where(tbl.ID.Eq(userID)).First() if err != nil { return nil, errors.Wrapf(err, "FindByID failed, %d", userID) } diff --git a/backend/app/services/user_test.go b/backend/app/services/user_test.go index cedaacb..55de731 100644 --- a/backend/app/services/user_test.go +++ b/backend/app/services/user_test.go @@ -9,6 +9,7 @@ import ( "quyun/v2/database" "quyun/v2/database/models" "quyun/v2/pkg/consts" + "quyun/v2/pkg/utils" "github.com/samber/lo" . "github.com/smartystreets/goconvey/convey" @@ -200,3 +201,62 @@ func (t *UserTestSuite) Test_Page() { }) }) } + +func (t *UserTestSuite) Test_Relations() { + Convey("test page", t.T(), func() { + // database.Truncate( + // t.T().Context(), + // t.DB, + // models.TableNameUser, + // models.TableNameTenant, + // models.TableNameTenantUser, + // ) + + // username := "test-user" + // mUser01 := &models.User{ + // Username: username, + // Password: "test-password", + // Roles: types.NewArray([]consts.Role{consts.RoleUser}), + // Status: consts.UserStatusPendingVerify, + // } + + // err := mUser01.Create(t.T().Context()) + // So(err, ShouldBeNil) + + // tenantModel := &models.Tenant{ + // UserID: 1, + // Code: "abc", + // UUID: types.NewUUIDv4(), + // Name: "T01", + // Status: consts.TenantStatusVerified, + // } + + // err = tenantModel.Create(t.T().Context()) + // So(err, ShouldBeNil) + + // count := 10 + // for i := 0; i < count; i++ { + // mUser := &models.User{ + // Username: fmt.Sprintf("user_%d", i), + // Password: "test-password", + // Roles: types.NewArray([]consts.Role{consts.RoleUser}), + // Status: consts.UserStatusPendingVerify, + // } + + // err = mUser.Create(t.T().Context()) + // So(err, ShouldBeNil) + + // // create tenant user + // err = Tenant.AddUser(t.T().Context(), 1, mUser.ID) + // So(err, ShouldBeNil) + // } + + Convey("filter tenant users", func() { + m, err := User.FindByID(t.T().Context(), 1) + So(err, ShouldBeNil) + // So(m.OwnedTenant, ShouldNotBeNil) + // So(m.Tenants, ShouldHaveLength, 10) + t.T().Logf("%s", utils.MustJsonString(m)) + }) + }) +} diff --git a/backend/database/.transform.yaml b/backend/database/.transform.yaml index 97db49e..cfe0699 100644 --- a/backend/database/.transform.yaml +++ b/backend/database/.transform.yaml @@ -20,3 +20,23 @@ field_type: role: types.Array[consts.TenantUserRole] status: consts.UserStatus field_relate: + users: + OwnedTenant: + relation: belongs_to + table: tenants + json: owned + Tenants: + json: tenants + relation: many_to_many + table: tenants + pivot: tenant_users + # foreign_key: user_id # 当前表(users)用于关联的键(转为结构体字段名 user_id) + join_foreign_key: user_id # 中间表中指向当前表的列(tenant_users.user_id) + # references: id # 关联表(tenants)被引用的列(转为结构体字段名 ID) + join_references: tenant_id # 中间表中指向关联表的列(tenant_users.tenant_id) + tenants: + Users: + relation: many_to_many + table: users + pivot: tenant_users + json: users diff --git a/backend/database/models/tenants.gen.go b/backend/database/models/tenants.gen.go index 329451b..43674eb 100644 --- a/backend/database/models/tenants.gen.go +++ b/backend/database/models/tenants.gen.go @@ -28,6 +28,7 @@ type Tenant struct { ExpiredAt time.Time `gorm:"column:expired_at;type:timestamp with time zone" json:"expired_at"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now()" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now()" json:"updated_at"` + Users []*User `gorm:"many2many:tenant_users" json:"users,omitempty"` } // Quick operations without importing query package diff --git a/backend/database/models/tenants.query.gen.go b/backend/database/models/tenants.query.gen.go index 7393e6a..eee716b 100644 --- a/backend/database/models/tenants.query.gen.go +++ b/backend/database/models/tenants.query.gen.go @@ -35,6 +35,11 @@ func newTenant(db *gorm.DB, opts ...gen.DOOption) tenantQuery { _tenantQuery.ExpiredAt = field.NewTime(tableName, "expired_at") _tenantQuery.CreatedAt = field.NewTime(tableName, "created_at") _tenantQuery.UpdatedAt = field.NewTime(tableName, "updated_at") + _tenantQuery.Users = tenantQueryManyToManyUsers{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Users", "User"), + } _tenantQuery.fillFieldMap() @@ -55,6 +60,7 @@ type tenantQuery struct { ExpiredAt field.Time CreatedAt field.Time UpdatedAt field.Time + Users tenantQueryManyToManyUsers fieldMap map[string]field.Expr } @@ -111,7 +117,7 @@ func (t *tenantQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (t *tenantQuery) fillFieldMap() { - t.fieldMap = make(map[string]field.Expr, 10) + t.fieldMap = make(map[string]field.Expr, 11) t.fieldMap["id"] = t.ID t.fieldMap["user_id"] = t.UserID t.fieldMap["code"] = t.Code @@ -122,18 +128,103 @@ func (t *tenantQuery) fillFieldMap() { t.fieldMap["expired_at"] = t.ExpiredAt t.fieldMap["created_at"] = t.CreatedAt t.fieldMap["updated_at"] = t.UpdatedAt + } func (t tenantQuery) clone(db *gorm.DB) tenantQuery { t.tenantQueryDo.ReplaceConnPool(db.Statement.ConnPool) + t.Users.db = db.Session(&gorm.Session{Initialized: true}) + t.Users.db.Statement.ConnPool = db.Statement.ConnPool return t } func (t tenantQuery) replaceDB(db *gorm.DB) tenantQuery { t.tenantQueryDo.ReplaceDB(db) + t.Users.db = db.Session(&gorm.Session{}) return t } +type tenantQueryManyToManyUsers struct { + db *gorm.DB + + field.RelationField +} + +func (a tenantQueryManyToManyUsers) Where(conds ...field.Expr) *tenantQueryManyToManyUsers { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a tenantQueryManyToManyUsers) WithContext(ctx context.Context) *tenantQueryManyToManyUsers { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a tenantQueryManyToManyUsers) Session(session *gorm.Session) *tenantQueryManyToManyUsers { + a.db = a.db.Session(session) + return &a +} + +func (a tenantQueryManyToManyUsers) Model(m *Tenant) *tenantQueryManyToManyUsersTx { + return &tenantQueryManyToManyUsersTx{a.db.Model(m).Association(a.Name())} +} + +func (a tenantQueryManyToManyUsers) Unscoped() *tenantQueryManyToManyUsers { + a.db = a.db.Unscoped() + return &a +} + +type tenantQueryManyToManyUsersTx struct{ tx *gorm.Association } + +func (a tenantQueryManyToManyUsersTx) Find() (result []*User, err error) { + return result, a.tx.Find(&result) +} + +func (a tenantQueryManyToManyUsersTx) Append(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a tenantQueryManyToManyUsersTx) Replace(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a tenantQueryManyToManyUsersTx) Delete(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a tenantQueryManyToManyUsersTx) Clear() error { + return a.tx.Clear() +} + +func (a tenantQueryManyToManyUsersTx) Count() int64 { + return a.tx.Count() +} + +func (a tenantQueryManyToManyUsersTx) Unscoped() *tenantQueryManyToManyUsersTx { + a.tx = a.tx.Unscoped() + return &a +} + type tenantQueryDo struct{ gen.DO } func (t tenantQueryDo) Debug() *tenantQueryDo { diff --git a/backend/database/models/users.gen.go b/backend/database/models/users.gen.go index cfacc8b..72844a7 100644 --- a/backend/database/models/users.gen.go +++ b/backend/database/models/users.gen.go @@ -19,16 +19,18 @@ const TableNameUser = "users" // User mapped from table type User struct { - ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now()" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now()" json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` - Username string `gorm:"column:username;type:character varying(255);not null" json:"username"` - Password string `gorm:"column:password;type:character varying(255);not null" json:"password"` - Roles types.Array[consts.Role] `gorm:"column:roles;type:text[];not null;default:ARRAY['user" json:"roles"` - Status consts.UserStatus `gorm:"column:status;type:character varying(50);not null;default:active" json:"status"` - Metas types.JSON `gorm:"column:metas;type:jsonb;not null;default:{}" json:"metas"` - VerifiedAt time.Time `gorm:"column:verified_at;type:timestamp with time zone" json:"verified_at"` + ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:now()" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:now()" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` + Username string `gorm:"column:username;type:character varying(255);not null" json:"username"` + Password string `gorm:"column:password;type:character varying(255);not null" json:"password"` + Roles types.Array[consts.Role] `gorm:"column:roles;type:text[];not null;default:ARRAY['user" json:"roles"` + Status consts.UserStatus `gorm:"column:status;type:character varying(50);not null;default:active" json:"status"` + Metas types.JSON `gorm:"column:metas;type:jsonb;not null;default:{}" json:"metas"` + VerifiedAt time.Time `gorm:"column:verified_at;type:timestamp with time zone" json:"verified_at"` + OwnedTenant *Tenant `json:"owned,omitempty"` + Tenants []*Tenant `gorm:"joinForeignKey:UserID;joinReferences:TenantID;many2many:tenant_users" json:"tenants,omitempty"` } // Quick operations without importing query package diff --git a/backend/database/models/users.query.gen.go b/backend/database/models/users.query.gen.go index 57c0184..5a1f7f5 100644 --- a/backend/database/models/users.query.gen.go +++ b/backend/database/models/users.query.gen.go @@ -35,6 +35,17 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) userQuery { _userQuery.Status = field.NewField(tableName, "status") _userQuery.Metas = field.NewJSONB(tableName, "metas") _userQuery.VerifiedAt = field.NewTime(tableName, "verified_at") + _userQuery.OwnedTenant = userQueryBelongsToOwnedTenant{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("OwnedTenant", "Tenant"), + } + + _userQuery.Tenants = userQueryManyToManyTenants{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Tenants", "Tenant"), + } _userQuery.fillFieldMap() @@ -44,17 +55,20 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) userQuery { type userQuery struct { userQueryDo userQueryDo - ALL field.Asterisk - ID field.Int64 - CreatedAt field.Time - UpdatedAt field.Time - DeletedAt field.Field - Username field.String - Password field.String - Roles field.Array - Status field.Field - Metas field.JSONB - VerifiedAt field.Time + ALL field.Asterisk + ID field.Int64 + CreatedAt field.Time + UpdatedAt field.Time + DeletedAt field.Field + Username field.String + Password field.String + Roles field.Array + Status field.Field + Metas field.JSONB + VerifiedAt field.Time + OwnedTenant userQueryBelongsToOwnedTenant + + Tenants userQueryManyToManyTenants fieldMap map[string]field.Expr } @@ -111,7 +125,7 @@ func (u *userQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (u *userQuery) fillFieldMap() { - u.fieldMap = make(map[string]field.Expr, 10) + u.fieldMap = make(map[string]field.Expr, 12) u.fieldMap["id"] = u.ID u.fieldMap["created_at"] = u.CreatedAt u.fieldMap["updated_at"] = u.UpdatedAt @@ -122,18 +136,187 @@ func (u *userQuery) fillFieldMap() { u.fieldMap["status"] = u.Status u.fieldMap["metas"] = u.Metas u.fieldMap["verified_at"] = u.VerifiedAt + } func (u userQuery) clone(db *gorm.DB) userQuery { u.userQueryDo.ReplaceConnPool(db.Statement.ConnPool) + u.OwnedTenant.db = db.Session(&gorm.Session{Initialized: true}) + u.OwnedTenant.db.Statement.ConnPool = db.Statement.ConnPool + u.Tenants.db = db.Session(&gorm.Session{Initialized: true}) + u.Tenants.db.Statement.ConnPool = db.Statement.ConnPool return u } func (u userQuery) replaceDB(db *gorm.DB) userQuery { u.userQueryDo.ReplaceDB(db) + u.OwnedTenant.db = db.Session(&gorm.Session{}) + u.Tenants.db = db.Session(&gorm.Session{}) return u } +type userQueryBelongsToOwnedTenant struct { + db *gorm.DB + + field.RelationField +} + +func (a userQueryBelongsToOwnedTenant) Where(conds ...field.Expr) *userQueryBelongsToOwnedTenant { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a userQueryBelongsToOwnedTenant) WithContext(ctx context.Context) *userQueryBelongsToOwnedTenant { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a userQueryBelongsToOwnedTenant) Session(session *gorm.Session) *userQueryBelongsToOwnedTenant { + a.db = a.db.Session(session) + return &a +} + +func (a userQueryBelongsToOwnedTenant) Model(m *User) *userQueryBelongsToOwnedTenantTx { + return &userQueryBelongsToOwnedTenantTx{a.db.Model(m).Association(a.Name())} +} + +func (a userQueryBelongsToOwnedTenant) Unscoped() *userQueryBelongsToOwnedTenant { + a.db = a.db.Unscoped() + return &a +} + +type userQueryBelongsToOwnedTenantTx struct{ tx *gorm.Association } + +func (a userQueryBelongsToOwnedTenantTx) Find() (result *Tenant, err error) { + return result, a.tx.Find(&result) +} + +func (a userQueryBelongsToOwnedTenantTx) Append(values ...*Tenant) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a userQueryBelongsToOwnedTenantTx) Replace(values ...*Tenant) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a userQueryBelongsToOwnedTenantTx) Delete(values ...*Tenant) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a userQueryBelongsToOwnedTenantTx) Clear() error { + return a.tx.Clear() +} + +func (a userQueryBelongsToOwnedTenantTx) Count() int64 { + return a.tx.Count() +} + +func (a userQueryBelongsToOwnedTenantTx) Unscoped() *userQueryBelongsToOwnedTenantTx { + a.tx = a.tx.Unscoped() + return &a +} + +type userQueryManyToManyTenants struct { + db *gorm.DB + + field.RelationField +} + +func (a userQueryManyToManyTenants) Where(conds ...field.Expr) *userQueryManyToManyTenants { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a userQueryManyToManyTenants) WithContext(ctx context.Context) *userQueryManyToManyTenants { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a userQueryManyToManyTenants) Session(session *gorm.Session) *userQueryManyToManyTenants { + a.db = a.db.Session(session) + return &a +} + +func (a userQueryManyToManyTenants) Model(m *User) *userQueryManyToManyTenantsTx { + return &userQueryManyToManyTenantsTx{a.db.Model(m).Association(a.Name())} +} + +func (a userQueryManyToManyTenants) Unscoped() *userQueryManyToManyTenants { + a.db = a.db.Unscoped() + return &a +} + +type userQueryManyToManyTenantsTx struct{ tx *gorm.Association } + +func (a userQueryManyToManyTenantsTx) Find() (result []*Tenant, err error) { + return result, a.tx.Find(&result) +} + +func (a userQueryManyToManyTenantsTx) Append(values ...*Tenant) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a userQueryManyToManyTenantsTx) Replace(values ...*Tenant) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a userQueryManyToManyTenantsTx) Delete(values ...*Tenant) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a userQueryManyToManyTenantsTx) Clear() error { + return a.tx.Clear() +} + +func (a userQueryManyToManyTenantsTx) Count() int64 { + return a.tx.Count() +} + +func (a userQueryManyToManyTenantsTx) Unscoped() *userQueryManyToManyTenantsTx { + a.tx = a.tx.Unscoped() + return &a +} + type userQueryDo struct{ gen.DO } func (u userQueryDo) Debug() *userQueryDo { diff --git a/backend/pkg/utils/json.go b/backend/pkg/utils/json.go new file mode 100644 index 0000000..ad92a22 --- /dev/null +++ b/backend/pkg/utils/json.go @@ -0,0 +1,9 @@ +package utils + +import "encoding/json" + +// MustString +func MustJsonString(in any) string { + b, _ := json.Marshal(in) + return string(b) +}