diff --git a/backend/app/http/super/dto/tenant.go b/backend/app/http/super/dto/tenant.go new file mode 100644 index 0000000..b6ed60d --- /dev/null +++ b/backend/app/http/super/dto/tenant.go @@ -0,0 +1,20 @@ +package dto + +import ( + "quyun/v2/app/requests" + "quyun/v2/database/models" +) + +type TenantFilter struct { + requests.Pagination + requests.SortQueryFilter + + Name *string `json:"name,omitempty"` +} + +type TenantItem struct { + *models.Tenant + + UserCount int64 + UserBalance int64 +} diff --git a/backend/app/http/super/provider.gen.go b/backend/app/http/super/provider.gen.go index 2e01ffb..0cad4ab 100755 --- a/backend/app/http/super/provider.gen.go +++ b/backend/app/http/super/provider.gen.go @@ -26,9 +26,11 @@ func Provide(opts ...opt.Option) error { } if err := container.Container.Provide(func( authController *authController, + tenant *tenant, ) (contracts.HttpRoute, error) { obj := &Routes{ authController: authController, + tenant: tenant, } if err := obj.Prepare(); err != nil { return nil, err @@ -38,5 +40,12 @@ func Provide(opts ...opt.Option) error { }, atom.GroupRoutes); err != nil { return err } + if err := container.Container.Provide(func() (*tenant, error) { + obj := &tenant{} + + return obj, nil + }); err != nil { + return err + } return nil } diff --git a/backend/app/http/super/routes.gen.go b/backend/app/http/super/routes.gen.go index 2edb55a..f16aeee 100644 --- a/backend/app/http/super/routes.gen.go +++ b/backend/app/http/super/routes.gen.go @@ -5,12 +5,13 @@ package super import ( + "quyun/v2/app/http/super/dto" + "github.com/gofiber/fiber/v3" log "github.com/sirupsen/logrus" _ "go.ipao.vip/atom" _ "go.ipao.vip/atom/contracts" . "go.ipao.vip/atom/fen" - "quyun/v2/app/http/super/dto" ) // Routes implements the HttpRoute contract and provides route registration @@ -21,6 +22,7 @@ type Routes struct { log *log.Entry `inject:"false"` // Controller instances authController *authController + tenant *tenant } // Prepare initializes the routes provider with logging configuration. @@ -44,6 +46,12 @@ func (r *Routes) Register(router fiber.Router) { r.authController.login, Body[dto.LoginForm]("form"), )) + // Register routes for controller: tenant + r.log.Debugf("Registering route: Get /super/v1/tenants -> tenant.list") + router.Get("/super/v1/tenants", DataFunc1( + r.tenant.list, + Query[dto.TenantFilter]("filter"), + )) r.log.Info("Successfully registered all routes") } diff --git a/backend/app/http/super/tenant.go b/backend/app/http/super/tenant.go new file mode 100644 index 0000000..76b9bcc --- /dev/null +++ b/backend/app/http/super/tenant.go @@ -0,0 +1,19 @@ +package super + +import ( + "quyun/v2/app/http/super/dto" + "quyun/v2/app/requests" + "quyun/v2/app/services" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type tenant struct{} + +// list +// @Router /super/v1/tenants [get] +// @Bind filter query +func (*tenant) list(ctx fiber.Ctx, filter *dto.TenantFilter) (*requests.Pager, error) { + return services.Tenant.Pager(ctx, filter) +} diff --git a/backend/app/requests/pagination.go b/backend/app/requests/pagination.go index 20f1b51..c739bc7 100644 --- a/backend/app/requests/pagination.go +++ b/backend/app/requests/pagination.go @@ -2,10 +2,10 @@ package requests import "github.com/samber/lo" -type Pager[T any] struct { +type Pager struct { Pagination ` json:",inline"` Total int64 `json:"total"` - Items []T `json:"items"` + Items any `json:"items"` } type Pagination struct { diff --git a/backend/app/services/tenant.go b/backend/app/services/tenant.go index 6c81aeb..e6c4d45 100644 --- a/backend/app/services/tenant.go +++ b/backend/app/services/tenant.go @@ -3,10 +3,15 @@ package services import ( "context" + "quyun/v2/app/http/super/dto" + "quyun/v2/app/requests" + "quyun/v2/database" "quyun/v2/database/models" "quyun/v2/pkg/consts" "github.com/pkg/errors" + "github.com/samber/lo" + "go.ipao.vip/gen" ) // @provider @@ -65,3 +70,98 @@ func (t *tenant) SetUserRole(ctx context.Context, tenantID, userID int64, role . } return nil } + +// Pager +func (t *tenant) Pager(ctx context.Context, filter *dto.TenantFilter) (*requests.Pager, error) { + tbl, query := models.TenantQuery.QueryContext(ctx) + + conds := []gen.Condition{} + if filter.Name != nil { + conds = append(conds, tbl.Name.Like(database.WrapLike(*filter.Name))) + } + + filter.Pagination.Format() + mm, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit)) + if err != nil { + return nil, err + } + + tenantIds := lo.Map(mm, func(item *models.Tenant, _ int) int64 { return item.ID }) + + userCountMapping, err := t.TenantUserCountMapping(ctx, tenantIds) + if err != nil { + return nil, err + } + + userBalanceMapping, err := t.TenantUserBalanceMapping(ctx, tenantIds) + if err != nil { + return nil, err + } + + items := lo.Map(mm, func(model *models.Tenant, _ int) *dto.TenantItem { + return &dto.TenantItem{ + Tenant: model, + UserCount: lo.ValueOr(userCountMapping, model.ID, 0), + UserBalance: lo.ValueOr(userBalanceMapping, model.ID, 0), + } + }) + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: items, + }, nil +} + +func (t *tenant) TenantUserCountMapping(ctx context.Context, tenantIds []int64) (map[int64]int64, error) { + tbl, query := models.TenantUserQuery.QueryContext(ctx) + + var items []struct { + TenantID int64 + Count int64 + } + err := query. + Select( + tbl.TenantID, + tbl.UserID.Count().As("count"), + ). + Where(tbl.TenantID.In(tenantIds...)). + Group(tbl.TenantID). + Scan(&items) + if err != nil { + return nil, err + } + + result := make(map[int64]int64) + for _, item := range items { + result[item.TenantID] = item.Count + } + return result, nil +} + +// TenantUserBalanceMapping +func (t *tenant) TenantUserBalanceMapping(ctx context.Context, tenantIds []int64) (map[int64]int64, error) { + tbl, query := models.TenantUserQuery.QueryContext(ctx) + + var items []struct { + TenantID int64 + Balance int64 + } + err := query. + Select( + tbl.TenantID, + tbl.Balance.Sum().As("balance"), + ). + Where(tbl.TenantID.In(tenantIds...)). + Group(tbl.TenantID). + Scan(&items) + if err != nil { + return nil, err + } + + result := make(map[int64]int64) + for _, item := range items { + result[item.TenantID] = item.Balance + } + return result, nil +} diff --git a/backend/app/services/tenant_test.go b/backend/app/services/tenant_test.go new file mode 100644 index 0000000..0e0d548 --- /dev/null +++ b/backend/app/services/tenant_test.go @@ -0,0 +1,50 @@ +package services + +import ( + "database/sql" + "testing" + + "quyun/v2/app/commands/testx" + "quyun/v2/database" + "quyun/v2/database/models" + "quyun/v2/pkg/utils" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + + _ "go.ipao.vip/atom" + "go.ipao.vip/atom/contracts" + "go.uber.org/dig" +) + +type TenantTestSuiteInjectParams struct { + dig.In + + DB *sql.DB + Initials []contracts.Initial `group:"initials"` // nolint:structcheck +} + +type TenantTestSuite struct { + suite.Suite + + TenantTestSuiteInjectParams +} + +func Test_Tenant(t *testing.T) { + providers := testx.Default().With(Provide) + + testx.Serve(providers, t, func(p TenantTestSuiteInjectParams) { + suite.Run(t, &TenantTestSuite{TenantTestSuiteInjectParams: p}) + }) +} + +func (t *TenantTestSuite) Test_TenantUserCount() { + Convey("test get tenants user count", t.T(), func() { + database.Truncate(t.T().Context(), t.DB, models.TableNameTenant) + + result, err := Tenant.TenantUserCountMapping(t.T().Context(), []int64{1, 2}) + So(err, ShouldBeNil) + So(result, ShouldHaveLength, 2) + t.T().Logf("%s", utils.MustJsonString(result)) + }) +} diff --git a/backend/app/services/user.go b/backend/app/services/user.go index d52ee8d..b1dab86 100644 --- a/backend/app/services/user.go +++ b/backend/app/services/user.go @@ -61,7 +61,7 @@ type UserPageFilter struct { } // Page -func (t *user) Page(ctx context.Context, filter *UserPageFilter) (*requests.Pager[*models.User], error) { +func (t *user) Page(ctx context.Context, filter *UserPageFilter) (*requests.Pager, error) { tbl, query := models.UserQuery.QueryContext(ctx) conds := []gen.Condition{} @@ -81,8 +81,8 @@ func (t *user) Page(ctx context.Context, filter *UserPageFilter) (*requests.Page return nil, err } - return &requests.Pager[*models.User]{ - Pagination: requests.Pagination{}, + return &requests.Pager{ + Pagination: filter.Pagination, Total: total, Items: items, }, nil diff --git a/backend/database/migrations/20251216011456_tenant_users.sql b/backend/database/migrations/20251216011456_tenant_users.sql index fabf7d6..e6f722f 100644 --- a/backend/database/migrations/20251216011456_tenant_users.sql +++ b/backend/database/migrations/20251216011456_tenant_users.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS tenant_users( tenant_id bigint NOT NULL, user_id bigint NOT NULL, role TEXT[] NOT NULL DEFAULT ARRAY['member'], - balance numeric(20, 8) NOT NULL DEFAULT 0, + balance bigint NOT NULL DEFAULT 0, status varchar(50) NOT NULL DEFAULT 'active', created_at timestamptz NOT NULL DEFAULT NOW(), updated_at timestamptz NOT NULL DEFAULT NOW(), diff --git a/backend/database/models/tenant_users.gen.go b/backend/database/models/tenant_users.gen.go index b702eeb..44beba6 100644 --- a/backend/database/models/tenant_users.gen.go +++ b/backend/database/models/tenant_users.gen.go @@ -22,7 +22,7 @@ type TenantUser struct { TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` Role types.Array[consts.TenantUserRole] `gorm:"column:role;type:text[];not null;default:ARRAY['member" json:"role"` - Balance float64 `gorm:"column:balance;type:numeric(20,8);not null" json:"balance"` + Balance int64 `gorm:"column:balance;type:bigint;not null" json:"balance"` Status consts.UserStatus `gorm:"column:status;type:character varying(50);not null;default:active" json:"status"` 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"` diff --git a/backend/database/models/tenant_users.query.gen.go b/backend/database/models/tenant_users.query.gen.go index 3ed4202..fe083f9 100644 --- a/backend/database/models/tenant_users.query.gen.go +++ b/backend/database/models/tenant_users.query.gen.go @@ -29,7 +29,7 @@ func newTenantUser(db *gorm.DB, opts ...gen.DOOption) tenantUserQuery { _tenantUserQuery.TenantID = field.NewInt64(tableName, "tenant_id") _tenantUserQuery.UserID = field.NewInt64(tableName, "user_id") _tenantUserQuery.Role = field.NewArray(tableName, "role") - _tenantUserQuery.Balance = field.NewFloat64(tableName, "balance") + _tenantUserQuery.Balance = field.NewInt64(tableName, "balance") _tenantUserQuery.Status = field.NewField(tableName, "status") _tenantUserQuery.CreatedAt = field.NewTime(tableName, "created_at") _tenantUserQuery.UpdatedAt = field.NewTime(tableName, "updated_at") @@ -47,7 +47,7 @@ type tenantUserQuery struct { TenantID field.Int64 UserID field.Int64 Role field.Array - Balance field.Float64 + Balance field.Int64 Status field.Field CreatedAt field.Time UpdatedAt field.Time @@ -71,7 +71,7 @@ func (t *tenantUserQuery) updateTableName(table string) *tenantUserQuery { t.TenantID = field.NewInt64(table, "tenant_id") t.UserID = field.NewInt64(table, "user_id") t.Role = field.NewArray(table, "role") - t.Balance = field.NewFloat64(table, "balance") + t.Balance = field.NewInt64(table, "balance") t.Status = field.NewField(table, "status") t.CreatedAt = field.NewTime(table, "created_at") t.UpdatedAt = field.NewTime(table, "updated_at")