Files
quyun-v2/backend/app/services/tenant.go
2025-12-19 13:55:20 +08:00

374 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"context"
"strings"
"time"
"quyun/v2/app/http/super/dto"
tenantdto "quyun/v2/app/http/tenant/dto"
"quyun/v2/app/requests"
"quyun/v2/database"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
"github.com/pkg/errors"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
"go.ipao.vip/gen"
"go.ipao.vip/gen/types"
"gorm.io/gorm"
)
// tenant implements tenant-related domain operations.
//
// @provider
type tenant struct{}
// AdminTenantUsersPage 租户管理员分页查询成员列表(包含用户基础信息)。
func (t *tenant) AdminTenantUsersPage(ctx context.Context, tenantID int64, filter *tenantdto.AdminTenantUserListFilter) (*requests.Pager, error) {
if tenantID <= 0 {
return nil, errors.New("tenant_id must be > 0")
}
if filter == nil {
filter = &tenantdto.AdminTenantUserListFilter{}
}
filter.Pagination.Format()
tbl, query := models.TenantUserQuery.QueryContext(ctx)
conds := []gen.Condition{tbl.TenantID.Eq(tenantID)}
if filter.UserID != nil && *filter.UserID > 0 {
conds = append(conds, tbl.UserID.Eq(*filter.UserID))
}
if filter.Role != nil && *filter.Role != "" {
// role 字段为 PostgreSQL text[]:使用数组参数才能正确生成 `@> '{"tenant_admin"}'` 语义。
conds = append(conds, tbl.Role.Contains(types.NewArray([]consts.TenantUserRole{*filter.Role})))
}
if filter.Status != nil && *filter.Status != "" {
conds = append(conds, tbl.Status.Eq(*filter.Status))
}
if username := filter.UsernameTrimmed(); username != "" {
uTbl, _ := models.UserQuery.QueryContext(ctx)
query = query.LeftJoin(uTbl, uTbl.ID.EqCol(tbl.UserID))
conds = append(conds, uTbl.Username.Like(database.WrapLike(username)))
}
items, total, err := query.Where(conds...).Order(tbl.ID.Desc()).FindByPage(int(filter.Offset()), int(filter.Limit))
if err != nil {
return nil, err
}
userIDs := make([]int64, 0, len(items))
for _, tu := range items {
if tu == nil {
continue
}
userIDs = append(userIDs, tu.UserID)
}
var users []*models.User
if len(userIDs) > 0 {
uTbl, uQuery := models.UserQuery.QueryContext(ctx)
users, err = uQuery.Where(uTbl.ID.In(userIDs...)).Find()
if err != nil {
return nil, err
}
}
userMap := make(map[int64]*models.User, len(users))
for _, u := range users {
if u == nil {
continue
}
userMap[u.ID] = u
}
out := make([]*tenantdto.AdminTenantUserItem, 0, len(items))
for _, tu := range items {
if tu == nil {
continue
}
out = append(out, &tenantdto.AdminTenantUserItem{
TenantUser: tu,
User: userMap[tu.UserID],
})
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: out,
}, nil
}
func (t *tenant) ContainsUserID(ctx context.Context, tenantID, userID int64) (*models.User, error) {
tbl, query := models.TenantUserQuery.QueryContext(ctx)
_, err := query.Where(tbl.TenantID.Eq(tenantID), tbl.UserID.Eq(userID)).First()
if err != nil {
return nil, errors.Wrapf(err, "ContainsUserID failed, tenantID: %d, userID: %d", tenantID, userID)
}
return User.FindByID(ctx, userID)
}
// AddUser
func (t *tenant) AddUser(ctx context.Context, tenantID, userID int64) error {
logrus.WithFields(logrus.Fields{
"tenant_id": tenantID,
"user_id": userID,
}).Info("services.tenant.add_user")
// 幂等:若成员关系已存在,则直接返回成功,避免重复插入触发唯一约束错误。
tbl, query := models.TenantUserQuery.QueryContext(ctx)
_, err := query.Where(tbl.TenantID.Eq(tenantID), tbl.UserID.Eq(userID)).First()
if err == nil {
return nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrapf(err, "AddUser failed to query existing, tenantID: %d, userID: %d", tenantID, userID)
}
// 关键默认值:加入租户默认成为 member并设置为 verified避免 DB 默认值与枚举不一致导致脏数据)。
tenantUser := &models.TenantUser{
TenantID: tenantID,
UserID: userID,
Role: types.NewArray([]consts.TenantUserRole{consts.TenantUserRoleMember}),
Status: consts.UserStatusVerified,
}
if err := tenantUser.Create(ctx); err != nil {
return errors.Wrapf(err, "AddUser failed, tenantID: %d, userID: %d", tenantID, userID)
}
return nil
}
// RemoveUser
func (t *tenant) RemoveUser(ctx context.Context, tenantID, userID int64) error {
tbl, query := models.TenantUserQuery.QueryContext(ctx)
tenantUser, err := query.Where(tbl.TenantID.Eq(tenantID), tbl.UserID.Eq(userID)).First()
if err != nil {
// 幂等:成员不存在时也返回成功,便于后台重试/批量移除。
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return errors.Wrapf(err, "RemoveUser failed to find, tenantID: %d, userID: %d", tenantID, userID)
}
_, err = tenantUser.Delete(ctx)
if err != nil {
return errors.Wrapf(err, "RemoveUser failed to delete, tenantID: %d, userID: %d", tenantID, userID)
}
return nil
}
// SetUserRole
func (t *tenant) SetUserRole(ctx context.Context, tenantID, userID int64, role ...consts.TenantUserRole) error {
tbl, query := models.TenantUserQuery.QueryContext(ctx)
tenantUser, err := query.Where(tbl.TenantID.Eq(tenantID), tbl.UserID.Eq(userID)).First()
if err != nil {
return errors.Wrapf(err, "SetUserRole failed to find, tenantID: %d, userID: %d", tenantID, userID)
}
// 角色更新:当前约定 role 数组通常只存一个主角色member/tenant_admin
tenantUser.Role = types.NewArray(role)
if _, err := tenantUser.Update(ctx); err != nil {
return errors.Wrapf(err, "SetUserRole failed to update, tenantID: %d, userID: %d", tenantID, userID)
}
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)))
}
if filter.Status != nil {
conds = append(conds, tbl.Status.Eq(*filter.Status))
}
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),
StatusDescription: model.Status.Description(),
}
})
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: items,
}, nil
}
func (t *tenant) TenantUserCountMapping(ctx context.Context, tenantIds []int64) (map[int64]int64, error) {
// 关键语义:返回值必须包含入参中的所有 tenant_id。
// 即便该租户当前没有成员,也应返回 count=0便于调用方直接取值而无需额外补全逻辑。
result := make(map[int64]int64, len(tenantIds))
for _, id := range tenantIds {
if id <= 0 {
continue
}
result[id] = 0
}
if len(result) == 0 {
return result, nil
}
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
}
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) {
// 关键语义:返回值必须包含入参中的所有 tenant_id。
// 即便该租户当前没有成员,也应返回 balance=0保持调用方逻辑一致。
result := make(map[int64]int64, len(tenantIds))
for _, id := range tenantIds {
if id <= 0 {
continue
}
result[id] = 0
}
if len(result) == 0 {
return result, nil
}
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
}
for _, item := range items {
result[item.TenantID] = item.Balance
}
return result, nil
}
// FindByID
func (t *tenant) FindByID(ctx context.Context, id int64) (*models.Tenant, error) {
tbl, query := models.TenantQuery.QueryContext(ctx)
m, err := query.Where(tbl.ID.Eq(id)).First()
if err != nil {
return nil, errors.Wrapf(err, "find by id failed, id: %d", id)
}
return m, nil
}
func (t *tenant) FindByCode(ctx context.Context, code string) (*models.Tenant, error) {
code = strings.TrimSpace(code)
if code == "" {
return nil, errors.New("tenant code is empty")
}
code = strings.ToLower(code)
var m models.Tenant
err := models.Q.Tenant.WithContext(ctx).UnderlyingDB().Where("lower(code) = ?", code).First(&m).Error
if err != nil {
return nil, errors.Wrapf(err, "find by code failed, code: %s", code)
}
return &m, nil
}
func (t *tenant) FindTenantUser(ctx context.Context, tenantID, userID int64) (*models.TenantUser, error) {
logrus.WithField("tenant_id", tenantID).WithField("user_id", userID).Info("find tenant user")
tbl, query := models.TenantUserQuery.QueryContext(ctx)
m, err := query.Where(tbl.TenantID.Eq(tenantID), tbl.UserID.Eq(userID)).First()
if err != nil {
return nil, errors.Wrapf(err, "find tenant user failed, tenantID: %d, userID: %d", tenantID, userID)
}
return m, nil
}
// AddExpireDuration
func (t *tenant) AddExpireDuration(ctx context.Context, tenantID int64, duration time.Duration) error {
logrus.WithField("tenant_id", tenantID).WithField("duration", duration).Info("add expire duration")
m, err := t.FindByID(ctx, tenantID)
if err != nil {
return err
}
if m.ExpiredAt.Before(time.Now()) {
m.ExpiredAt = time.Now().Add(duration)
} else {
m.ExpiredAt = m.ExpiredAt.Add(duration)
}
return m.Save(ctx)
}
// UpdateStatus
func (t *tenant) UpdateStatus(ctx context.Context, tenantID int64, status consts.TenantStatus) error {
logrus.WithField("tenant_id", tenantID).WithField("status", status).Info("update tenant status")
m, err := t.FindByID(ctx, tenantID)
if err != nil {
return err
}
m.Status = status
_, err = m.Update(ctx)
if err != nil {
return err
}
return nil
}