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

View File

@@ -2,6 +2,7 @@ package super
import ( import (
"database/sql" "database/sql"
"quyun/v2/providers/app" "quyun/v2/providers/app"
"go.ipao.vip/atom" "go.ipao.vip/atom"

View File

@@ -16,7 +16,7 @@ type user struct{}
func (t *user) FindByID(ctx context.Context, userID int64) (*models.User, error) { func (t *user) FindByID(ctx context.Context, userID int64) (*models.User, error) {
tbl, query := models.UserQuery.QueryContext(ctx) 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 { if err != nil {
return nil, errors.Wrapf(err, "FindByID failed, %d", userID) return nil, errors.Wrapf(err, "FindByID failed, %d", userID)
} }

View File

@@ -9,6 +9,7 @@ import (
"quyun/v2/database" "quyun/v2/database"
"quyun/v2/database/models" "quyun/v2/database/models"
"quyun/v2/pkg/consts" "quyun/v2/pkg/consts"
"quyun/v2/pkg/utils"
"github.com/samber/lo" "github.com/samber/lo"
. "github.com/smartystreets/goconvey/convey" . "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))
})
})
}

View File

@@ -20,3 +20,23 @@ field_type:
role: types.Array[consts.TenantUserRole] role: types.Array[consts.TenantUserRole]
status: consts.UserStatus status: consts.UserStatus
field_relate: 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

View File

@@ -28,6 +28,7 @@ type Tenant struct {
ExpiredAt time.Time `gorm:"column:expired_at;type:timestamp with time zone" json:"expired_at"` 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"` 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"` 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 // Quick operations without importing query package

View File

@@ -35,6 +35,11 @@ func newTenant(db *gorm.DB, opts ...gen.DOOption) tenantQuery {
_tenantQuery.ExpiredAt = field.NewTime(tableName, "expired_at") _tenantQuery.ExpiredAt = field.NewTime(tableName, "expired_at")
_tenantQuery.CreatedAt = field.NewTime(tableName, "created_at") _tenantQuery.CreatedAt = field.NewTime(tableName, "created_at")
_tenantQuery.UpdatedAt = field.NewTime(tableName, "updated_at") _tenantQuery.UpdatedAt = field.NewTime(tableName, "updated_at")
_tenantQuery.Users = tenantQueryManyToManyUsers{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Users", "User"),
}
_tenantQuery.fillFieldMap() _tenantQuery.fillFieldMap()
@@ -55,6 +60,7 @@ type tenantQuery struct {
ExpiredAt field.Time ExpiredAt field.Time
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
Users tenantQueryManyToManyUsers
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@@ -111,7 +117,7 @@ func (t *tenantQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (t *tenantQuery) fillFieldMap() { 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["id"] = t.ID
t.fieldMap["user_id"] = t.UserID t.fieldMap["user_id"] = t.UserID
t.fieldMap["code"] = t.Code t.fieldMap["code"] = t.Code
@@ -122,18 +128,103 @@ func (t *tenantQuery) fillFieldMap() {
t.fieldMap["expired_at"] = t.ExpiredAt t.fieldMap["expired_at"] = t.ExpiredAt
t.fieldMap["created_at"] = t.CreatedAt t.fieldMap["created_at"] = t.CreatedAt
t.fieldMap["updated_at"] = t.UpdatedAt t.fieldMap["updated_at"] = t.UpdatedAt
} }
func (t tenantQuery) clone(db *gorm.DB) tenantQuery { func (t tenantQuery) clone(db *gorm.DB) tenantQuery {
t.tenantQueryDo.ReplaceConnPool(db.Statement.ConnPool) 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 return t
} }
func (t tenantQuery) replaceDB(db *gorm.DB) tenantQuery { func (t tenantQuery) replaceDB(db *gorm.DB) tenantQuery {
t.tenantQueryDo.ReplaceDB(db) t.tenantQueryDo.ReplaceDB(db)
t.Users.db = db.Session(&gorm.Session{})
return t 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 } type tenantQueryDo struct{ gen.DO }
func (t tenantQueryDo) Debug() *tenantQueryDo { func (t tenantQueryDo) Debug() *tenantQueryDo {

View File

@@ -19,16 +19,18 @@ const TableNameUser = "users"
// User mapped from table <users> // User mapped from table <users>
type User struct { type User struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` 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"` 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"` 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"` 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"` 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"` 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"` 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"` 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"` 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"` 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 // Quick operations without importing query package

View File

@@ -35,6 +35,17 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) userQuery {
_userQuery.Status = field.NewField(tableName, "status") _userQuery.Status = field.NewField(tableName, "status")
_userQuery.Metas = field.NewJSONB(tableName, "metas") _userQuery.Metas = field.NewJSONB(tableName, "metas")
_userQuery.VerifiedAt = field.NewTime(tableName, "verified_at") _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() _userQuery.fillFieldMap()
@@ -44,17 +55,20 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) userQuery {
type userQuery struct { type userQuery struct {
userQueryDo userQueryDo userQueryDo userQueryDo
ALL field.Asterisk ALL field.Asterisk
ID field.Int64 ID field.Int64
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
DeletedAt field.Field DeletedAt field.Field
Username field.String Username field.String
Password field.String Password field.String
Roles field.Array Roles field.Array
Status field.Field Status field.Field
Metas field.JSONB Metas field.JSONB
VerifiedAt field.Time VerifiedAt field.Time
OwnedTenant userQueryBelongsToOwnedTenant
Tenants userQueryManyToManyTenants
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@@ -111,7 +125,7 @@ func (u *userQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (u *userQuery) fillFieldMap() { 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["id"] = u.ID
u.fieldMap["created_at"] = u.CreatedAt u.fieldMap["created_at"] = u.CreatedAt
u.fieldMap["updated_at"] = u.UpdatedAt u.fieldMap["updated_at"] = u.UpdatedAt
@@ -122,18 +136,187 @@ func (u *userQuery) fillFieldMap() {
u.fieldMap["status"] = u.Status u.fieldMap["status"] = u.Status
u.fieldMap["metas"] = u.Metas u.fieldMap["metas"] = u.Metas
u.fieldMap["verified_at"] = u.VerifiedAt u.fieldMap["verified_at"] = u.VerifiedAt
} }
func (u userQuery) clone(db *gorm.DB) userQuery { func (u userQuery) clone(db *gorm.DB) userQuery {
u.userQueryDo.ReplaceConnPool(db.Statement.ConnPool) 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 return u
} }
func (u userQuery) replaceDB(db *gorm.DB) userQuery { func (u userQuery) replaceDB(db *gorm.DB) userQuery {
u.userQueryDo.ReplaceDB(db) u.userQueryDo.ReplaceDB(db)
u.OwnedTenant.db = db.Session(&gorm.Session{})
u.Tenants.db = db.Session(&gorm.Session{})
return u 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 } type userQueryDo struct{ gen.DO }
func (u userQueryDo) Debug() *userQueryDo { func (u userQueryDo) Debug() *userQueryDo {

View File

@@ -0,0 +1,9 @@
package utils
import "encoding/json"
// MustString
func MustJsonString(in any) string {
b, _ := json.Marshal(in)
return string(b)
}