feat: add user_tenant associations

This commit is contained in:
2025-12-16 14:14:58 +08:00
parent d058b7ffda
commit 4722eef72c
9 changed files with 391 additions and 24 deletions
+1
View File
@@ -2,6 +2,7 @@ package super
import (
"database/sql"
"quyun/v2/providers/app"
"go.ipao.vip/atom"
+1 -1
View File
@@ -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)
}
+60
View File
@@ -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))
})
})
}
+20
View File
@@ -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
+1
View File
@@ -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
+92 -1
View File
@@ -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 {
+12 -10
View File
@@ -19,16 +19,18 @@ const TableNameUser = "users"
// User mapped from table <users>
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
+195 -12
View File
@@ -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 {
+9
View File
@@ -0,0 +1,9 @@
package utils
import "encoding/json"
// MustString
func MustJsonString(in any) string {
b, _ := json.Marshal(in)
return string(b)
}