703 lines
21 KiB
Go
703 lines
21 KiB
Go
package admin
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gogf/gf/v2/database/gdb"
|
||
"github.com/gogf/gf/v2/errors/gerror"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/gtime"
|
||
|
||
v1 "github.com/qianfree/team-api/api/admin/v1"
|
||
"github.com/qianfree/team-api/internal/consts"
|
||
"github.com/qianfree/team-api/internal/dao"
|
||
"github.com/qianfree/team-api/internal/logic/billing"
|
||
"github.com/qianfree/team-api/internal/logic/common"
|
||
do "github.com/qianfree/team-api/internal/model/do"
|
||
"github.com/qianfree/team-api/internal/model/entity"
|
||
"github.com/qianfree/team-api/internal/utility/crypto"
|
||
"github.com/qianfree/team-api/internal/utility/export"
|
||
)
|
||
|
||
// TenantSelect returns a lightweight paginated tenant list for dropdown selectors.
|
||
func (s *sAdmin) TenantSelect(ctx context.Context, req *v1.TenantSelectReq) (*v1.TenantSelectRes, error) {
|
||
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
||
|
||
m := dao.TntTenants.Ctx(ctx)
|
||
if req.Keyword != "" {
|
||
keyword := "%" + strings.TrimSpace(req.Keyword) + "%"
|
||
m = m.Where("name LIKE ? OR code LIKE ?", keyword, keyword)
|
||
}
|
||
|
||
total, err := m.Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
m = dao.TntTenants.Ctx(ctx)
|
||
if req.Keyword != "" {
|
||
keyword := "%" + strings.TrimSpace(req.Keyword) + "%"
|
||
m = m.Where("name LIKE ? OR code LIKE ?", keyword, keyword)
|
||
}
|
||
|
||
var tenants []struct {
|
||
Id int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Code string `json:"code"`
|
||
}
|
||
err = m.Fields("id, name, code").OrderAsc("id").
|
||
Page(page, pageSize).
|
||
Scan(&tenants)
|
||
if err = common.IgnoreScanNoRows(err); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
items := make([]v1.TenantSelectItem, len(tenants))
|
||
for i, t := range tenants {
|
||
items[i] = v1.TenantSelectItem{
|
||
ID: t.Id,
|
||
Name: t.Name,
|
||
Code: t.Code,
|
||
}
|
||
}
|
||
|
||
return &v1.TenantSelectRes{
|
||
List: items,
|
||
Total: int(total),
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}, nil
|
||
}
|
||
|
||
// CreateTenant creates a new tenant with its owner user and wallet.
|
||
func (s *sAdmin) CreateTenant(ctx context.Context, req *v1.TenantCreateReq) (*v1.TenantCreateRes, error) {
|
||
tenantCode := strings.TrimSpace(strings.ToLower(req.TenantCode))
|
||
username := strings.TrimSpace(req.Username)
|
||
email := strings.TrimSpace(strings.ToLower(req.Email))
|
||
|
||
// Validate username format
|
||
if err := common.ValidateUsername(username); err != nil {
|
||
return nil, common.NewBusinessError(consts.CodeInvalidUsername, err.Error())
|
||
}
|
||
|
||
tenantName := strings.TrimSpace(req.TenantName)
|
||
if err := common.ValidateTenantName(tenantName); err != nil {
|
||
return nil, common.NewBusinessError(consts.CodeInvalidTenantName, err.Error())
|
||
}
|
||
|
||
count, err := dao.TntTenants.Ctx(ctx).
|
||
Where("code", tenantCode).Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if count > 0 {
|
||
return nil, common.NewBusinessError(consts.CodeTenantCodeExists, consts.MsgTenantCodeExists)
|
||
}
|
||
|
||
count, err = dao.TntUsers.Ctx(ctx).
|
||
Where("email", email).Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if count > 0 {
|
||
return nil, common.NewBadRequestError("邮箱已被使用")
|
||
}
|
||
|
||
if err := common.ValidatePassword(req.Password); err != nil {
|
||
return nil, common.NewBusinessError(consts.CodePasswordTooWeak, consts.MsgPasswordTooWeak)
|
||
}
|
||
|
||
passwordHash, err := crypto.HashPassword(req.Password)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// max_members 和 max_concurrency 默认为 nil(跟随等级配置)
|
||
var maxMembersVal *int
|
||
if req.MaxMembers != nil && *req.MaxMembers >= 1 {
|
||
maxMembersVal = req.MaxMembers
|
||
}
|
||
var maxConcurrencyVal *int
|
||
if req.MaxConcurrency != nil {
|
||
maxConcurrencyVal = req.MaxConcurrency
|
||
}
|
||
|
||
var tenantID int64
|
||
|
||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||
tenantResult, err := dao.TntTenants.Ctx(ctx).Data(do.TntTenants{
|
||
Name: tenantName,
|
||
Code: tenantCode,
|
||
MaxMembers: maxMembersVal,
|
||
MaxConcurrency: maxConcurrencyVal,
|
||
Level: 1,
|
||
Settings: "{}",
|
||
}).Insert()
|
||
if err != nil {
|
||
return gerror.Wrapf(err, "create tenant")
|
||
}
|
||
tenantID, err = tenantResult.LastInsertId()
|
||
if err != nil {
|
||
return gerror.Wrapf(err, "get tenant id")
|
||
}
|
||
|
||
userResult, err := dao.TntUsers.Ctx(ctx).Data(do.TntUsers{
|
||
TenantId: tenantID,
|
||
Username: username,
|
||
Email: email,
|
||
PasswordHash: passwordHash,
|
||
DisplayName: username,
|
||
Role: "owner",
|
||
}).Insert()
|
||
if err != nil {
|
||
return gerror.Wrapf(err, "create owner user")
|
||
}
|
||
ownerUserID, err := userResult.LastInsertId()
|
||
if err != nil {
|
||
return gerror.Wrapf(err, "get owner user id")
|
||
}
|
||
|
||
_, err = dao.TntTenants.Ctx(ctx).
|
||
Where("id", tenantID).
|
||
Data(do.TntTenants{
|
||
OwnerUserId: ownerUserID,
|
||
}).Update()
|
||
if err != nil {
|
||
return gerror.Wrapf(err, "set tenant owner")
|
||
}
|
||
|
||
warningThreshold := billing.Zero // 预警阈值默认 0 = 关闭,用户可自行开启
|
||
_, err = dao.BilWallets.Ctx(ctx).Data(do.BilWallets{
|
||
TenantId: tenantID,
|
||
Balance: 0,
|
||
FrozenBalance: 0,
|
||
WarningThreshold: &warningThreshold,
|
||
Currency: billing.Currency(ctx),
|
||
}).Insert()
|
||
if err != nil {
|
||
return gerror.Wrapf(err, "create wallet")
|
||
}
|
||
|
||
// Assign default model groups
|
||
var defaultGroups []struct {
|
||
Id int64
|
||
}
|
||
if err := dao.MdlModelGroups.Ctx(ctx).
|
||
Where("is_default", true).Where("status", "active").
|
||
Fields("id").Scan(&defaultGroups); err == nil && len(defaultGroups) > 0 {
|
||
insertData := make([]do.MdlTenantGroups, 0, len(defaultGroups))
|
||
for _, dg := range defaultGroups {
|
||
insertData = append(insertData, do.MdlTenantGroups{
|
||
TenantId: tenantID,
|
||
GroupId: dg.Id,
|
||
})
|
||
}
|
||
if _, err := dao.MdlTenantGroups.Ctx(ctx).
|
||
Batch(len(insertData)).Insert(insertData); err != nil {
|
||
g.Log().Warningf(ctx, "assign default model groups failed: %v", err)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &v1.TenantCreateRes{Id: tenantID}, nil
|
||
}
|
||
|
||
// batchTenantAggregates 批量查询一批租户的 owner 名称、成员数、钱包余额,
|
||
// 用三条聚合查询替代逐行 N+1 查询。
|
||
// 返回:ownerNames(ownerUserID→名称)、memberCounts(tenantID→成员数)、walletBalances(tenantID→余额)。
|
||
func batchTenantAggregates(ctx context.Context, tenantIDs, ownerIDs []int64) (
|
||
ownerNames map[int64]string,
|
||
memberCounts map[int64]int,
|
||
walletBalances map[int64]string,
|
||
) {
|
||
ownerNames = make(map[int64]string, len(ownerIDs))
|
||
memberCounts = make(map[int64]int, len(tenantIDs))
|
||
walletBalances = make(map[int64]string, len(tenantIDs))
|
||
|
||
// owner 名称:按 owner_user_id 批量取
|
||
if len(ownerIDs) > 0 {
|
||
var owners []struct {
|
||
Id int64 `json:"id"`
|
||
DisplayName string `json:"display_name"`
|
||
}
|
||
_ = dao.TntUsers.Ctx(ctx).Fields("id, display_name").WhereIn("id", ownerIDs).Scan(&owners)
|
||
for _, o := range owners {
|
||
ownerNames[o.Id] = o.DisplayName
|
||
}
|
||
}
|
||
|
||
if len(tenantIDs) > 0 {
|
||
// 成员数:按 tenant_id 分组统计
|
||
var counts []struct {
|
||
TenantID int64 `json:"tenant_id"`
|
||
Count int `json:"count"`
|
||
}
|
||
_ = dao.TntUsers.Ctx(ctx).Fields("tenant_id, COUNT(*) AS count").
|
||
WhereIn("tenant_id", tenantIDs).Group("tenant_id").Scan(&counts)
|
||
for _, c := range counts {
|
||
memberCounts[c.TenantID] = c.Count
|
||
}
|
||
|
||
// 钱包余额:按 tenant_id 批量取
|
||
var wallets []struct {
|
||
TenantID int64 `json:"tenant_id"`
|
||
Balance string `json:"balance"`
|
||
}
|
||
_ = dao.BilWallets.Ctx(ctx).Fields("tenant_id, balance").
|
||
WhereIn("tenant_id", tenantIDs).Scan(&wallets)
|
||
for _, w := range wallets {
|
||
walletBalances[w.TenantID] = w.Balance
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// ListTenants returns a paginated list of tenants.
|
||
func (s *sAdmin) ListTenants(ctx context.Context, req *v1.TenantListReq) (*v1.TenantListRes, error) {
|
||
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
|
||
|
||
m := dao.TntTenants.Ctx(ctx)
|
||
|
||
if req.Keyword != "" {
|
||
keyword := "%" + strings.TrimSpace(req.Keyword) + "%"
|
||
m = m.Where("name LIKE ? OR code LIKE ?", keyword, keyword)
|
||
}
|
||
if req.Status != "" {
|
||
m = m.Where("status", req.Status)
|
||
}
|
||
|
||
total, err := m.Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Rebuild model for data query
|
||
m = dao.TntTenants.Ctx(ctx)
|
||
if req.Keyword != "" {
|
||
keyword := "%" + strings.TrimSpace(req.Keyword) + "%"
|
||
m = m.Where("name LIKE ? OR code LIKE ?", keyword, keyword)
|
||
}
|
||
if req.Status != "" {
|
||
m = m.Where("status", req.Status)
|
||
}
|
||
|
||
var tenants []struct {
|
||
Id int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Code string `json:"code"`
|
||
LogoURL string `json:"logo_url"`
|
||
OwnerUserID int64 `json:"owner_user_id"`
|
||
Status string `json:"status"`
|
||
MaxMembers *int `json:"max_members"`
|
||
MaxConcurrency *int `json:"max_concurrency"`
|
||
DefaultChannelScope string `json:"default_channel_scope"`
|
||
Settings string `json:"settings"`
|
||
Level int `json:"level"`
|
||
CreatedAt *gtime.Time `json:"created_at"`
|
||
UpdatedAt *gtime.Time `json:"updated_at"`
|
||
}
|
||
err = m.OrderDesc("id").
|
||
Page(page, pageSize).
|
||
Scan(&tenants)
|
||
if err = common.IgnoreScanNoRows(err); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 批量获取等级配置(避免 N+1)
|
||
var levelConfigs []*entity.TntTenantLevelConfigs
|
||
dao.TntTenantLevelConfigs.Ctx(ctx).OrderAsc("level").Scan(&levelConfigs)
|
||
levelNameMap := make(map[int]string, len(levelConfigs))
|
||
levelMaxMembersMap := make(map[int]int, len(levelConfigs))
|
||
levelMaxConcMap := make(map[int]int, len(levelConfigs))
|
||
for _, lc := range levelConfigs {
|
||
levelNameMap[lc.Level] = lc.Name
|
||
levelMaxMembersMap[lc.Level] = lc.MaxMembers
|
||
levelMaxConcMap[lc.Level] = lc.MaxConcurrency
|
||
}
|
||
|
||
items := make([]v1.TenantItem, len(tenants))
|
||
|
||
// 批量查询 owner 名称 / 成员数 / 钱包余额(避免逐行 N+1)
|
||
tenantIDs := make([]int64, len(tenants))
|
||
ownerIDs := make([]int64, len(tenants))
|
||
for i, t := range tenants {
|
||
tenantIDs[i] = t.Id
|
||
ownerIDs[i] = t.OwnerUserID
|
||
}
|
||
ownerNameMap, memberCountMap, walletBalanceMap := batchTenantAggregates(ctx, tenantIDs, ownerIDs)
|
||
|
||
for i, t := range tenants {
|
||
item := v1.TenantItem{
|
||
ID: t.Id,
|
||
Name: t.Name,
|
||
Code: t.Code,
|
||
LogoURL: t.LogoURL,
|
||
OwnerUserID: t.OwnerUserID,
|
||
Status: t.Status,
|
||
MaxMembers: t.MaxMembers,
|
||
MaxConcurrency: t.MaxConcurrency,
|
||
DefaultChannelScope: t.DefaultChannelScope,
|
||
Level: t.Level,
|
||
LevelName: levelNameMap[t.Level],
|
||
CreatedAt: t.CreatedAt.String(),
|
||
UpdatedAt: t.UpdatedAt.String(),
|
||
}
|
||
|
||
// 计算实际生效值
|
||
if t.MaxMembers != nil {
|
||
item.EffectiveMaxMembers = *t.MaxMembers
|
||
} else if v, ok := levelMaxMembersMap[t.Level]; ok {
|
||
item.EffectiveMaxMembers = v
|
||
} else {
|
||
item.EffectiveMaxMembers = 10
|
||
}
|
||
if t.MaxConcurrency != nil {
|
||
item.EffectiveMaxConcurrency = *t.MaxConcurrency
|
||
} else if v, ok := levelMaxConcMap[t.Level]; ok {
|
||
item.EffectiveMaxConcurrency = v
|
||
} else {
|
||
item.EffectiveMaxConcurrency = 0
|
||
}
|
||
|
||
// 从批量结果中取 owner 名称 / 成员数 / 钱包余额
|
||
item.OwnerName = ownerNameMap[t.OwnerUserID]
|
||
item.MemberCount = memberCountMap[t.Id]
|
||
if bal, ok := walletBalanceMap[t.Id]; ok && bal != "" {
|
||
item.WalletBalance = bal
|
||
} else {
|
||
item.WalletBalance = "0"
|
||
}
|
||
|
||
items[i] = item
|
||
}
|
||
|
||
return &v1.TenantListRes{
|
||
List: items,
|
||
Total: int(total),
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}, nil
|
||
}
|
||
|
||
// GetTenant returns detail of a single tenant.
|
||
func (s *sAdmin) GetTenant(ctx context.Context, req *v1.TenantGetReq) (*v1.TenantGetRes, error) {
|
||
var tenant *struct {
|
||
Id int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Code string `json:"code"`
|
||
LogoURL string `json:"logo_url"`
|
||
OwnerUserID int64 `json:"owner_user_id"`
|
||
Status string `json:"status"`
|
||
MaxMembers *int `json:"max_members"`
|
||
MaxConcurrency *int `json:"max_concurrency"`
|
||
DefaultChannelScope string `json:"default_channel_scope"`
|
||
Settings string `json:"settings"`
|
||
Level int `json:"level"`
|
||
CreatedAt *gtime.Time `json:"created_at"`
|
||
UpdatedAt *gtime.Time `json:"updated_at"`
|
||
}
|
||
err := dao.TntTenants.Ctx(ctx).
|
||
Where("id", req.Id).Scan(&tenant)
|
||
if err = common.IgnoreScanNoRows(err); err != nil {
|
||
return nil, err
|
||
}
|
||
if tenant == nil {
|
||
return nil, common.NewNotFoundError("租户")
|
||
}
|
||
|
||
// Get owner name
|
||
var owner *struct {
|
||
DisplayName string `json:"display_name"`
|
||
}
|
||
_ = dao.TntUsers.Ctx(ctx).
|
||
Where("id", tenant.OwnerUserID).Scan(&owner)
|
||
|
||
// Get member count
|
||
memberCount, _ := dao.TntUsers.Ctx(ctx).
|
||
Where("tenant_id", req.Id).Count()
|
||
|
||
// Get wallet balance:优先读 Redis 权威值(调整余额/入账后详情页立即可见),DB 物化值兜底
|
||
walletBalance := "0"
|
||
if w, err := billing.GetWallet(ctx, req.Id); err == nil {
|
||
walletBalance = strconv.FormatFloat(w.Balance, 'f', -1, 64)
|
||
} else {
|
||
var wallet *struct {
|
||
Balance string `json:"balance"`
|
||
}
|
||
_ = dao.BilWallets.Ctx(ctx).
|
||
Where("tenant_id", req.Id).Scan(&wallet)
|
||
|
||
if wallet != nil && wallet.Balance != "" {
|
||
walletBalance = wallet.Balance
|
||
}
|
||
}
|
||
ownerName := ""
|
||
if owner != nil {
|
||
ownerName = owner.DisplayName
|
||
}
|
||
|
||
// Get level name
|
||
levelName := ""
|
||
var levelConfig *entity.TntTenantLevelConfigs
|
||
_ = dao.TntTenantLevelConfigs.Ctx(ctx).Where("level", tenant.Level).Scan(&levelConfig)
|
||
if levelConfig != nil {
|
||
levelName = levelConfig.Name
|
||
}
|
||
|
||
// 计算实际生效值
|
||
effectiveMaxMembers := 10
|
||
effectiveMaxConc := 0
|
||
if tenant.MaxMembers != nil {
|
||
effectiveMaxMembers = *tenant.MaxMembers
|
||
} else if levelConfig != nil {
|
||
effectiveMaxMembers = levelConfig.MaxMembers
|
||
}
|
||
if tenant.MaxConcurrency != nil {
|
||
effectiveMaxConc = *tenant.MaxConcurrency
|
||
} else if levelConfig != nil {
|
||
effectiveMaxConc = levelConfig.MaxConcurrency
|
||
}
|
||
|
||
return &v1.TenantGetRes{
|
||
TenantItem: v1.TenantItem{
|
||
ID: tenant.Id,
|
||
Name: tenant.Name,
|
||
Code: tenant.Code,
|
||
LogoURL: tenant.LogoURL,
|
||
OwnerUserID: tenant.OwnerUserID,
|
||
OwnerName: ownerName,
|
||
Status: tenant.Status,
|
||
MaxMembers: tenant.MaxMembers,
|
||
MaxConcurrency: tenant.MaxConcurrency,
|
||
EffectiveMaxMembers: effectiveMaxMembers,
|
||
EffectiveMaxConcurrency: effectiveMaxConc,
|
||
DefaultChannelScope: tenant.DefaultChannelScope,
|
||
MemberCount: memberCount,
|
||
WalletBalance: walletBalance,
|
||
Level: tenant.Level,
|
||
LevelName: levelName,
|
||
CreatedAt: tenant.CreatedAt.String(),
|
||
UpdatedAt: tenant.UpdatedAt.String(),
|
||
},
|
||
Settings: tenant.Settings,
|
||
}, nil
|
||
}
|
||
|
||
// UpdateTenantStatus updates a tenant's status.
|
||
func (s *sAdmin) UpdateTenantStatus(ctx context.Context, req *v1.TenantUpdateStatusReq) (*v1.TenantUpdateStatusRes, error) {
|
||
if req.Status != "active" && req.Status != "suspended" && req.Status != "closed" {
|
||
return nil, common.NewBadRequestError("状态值无效")
|
||
}
|
||
|
||
// Check tenant exists
|
||
var tenant *struct {
|
||
Id int64 `json:"id"`
|
||
}
|
||
err := dao.TntTenants.Ctx(ctx).
|
||
Where("id", req.Id).Scan(&tenant)
|
||
if err = common.IgnoreScanNoRows(err); err != nil {
|
||
return nil, err
|
||
}
|
||
if tenant == nil {
|
||
return nil, common.NewNotFoundError("租户")
|
||
}
|
||
|
||
_, err = dao.TntTenants.Ctx(ctx).Where("id", req.Id).Update(do.TntTenants{
|
||
Status: req.Status,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return nil, nil
|
||
}
|
||
|
||
// UpdateTenant updates tenant information.
|
||
func (s *sAdmin) UpdateTenant(ctx context.Context, req *v1.TenantUpdateReq) (*v1.TenantUpdateRes, error) {
|
||
data := do.TntTenants{}
|
||
|
||
if req.Name != "" {
|
||
tenantName := strings.TrimSpace(req.Name)
|
||
if err := common.ValidateTenantName(tenantName); err != nil {
|
||
return nil, common.NewBusinessError(consts.CodeInvalidTenantName, err.Error())
|
||
}
|
||
data.Name = tenantName
|
||
}
|
||
if req.MaxMembers != nil {
|
||
if *req.MaxMembers < 1 {
|
||
return nil, common.NewBadRequestError("最大成员数不能小于1")
|
||
}
|
||
data.MaxMembers = *req.MaxMembers
|
||
}
|
||
if req.MaxConcurrency != nil {
|
||
data.MaxConcurrency = *req.MaxConcurrency
|
||
}
|
||
|
||
// 管理员手动调整等级:仅更新等级,不自动填充成员数和并发数
|
||
// 成员数和并发数为 NULL 时自动跟随等级配置
|
||
if req.Level != nil {
|
||
var config *entity.TntTenantLevelConfigs
|
||
err := dao.TntTenantLevelConfigs.Ctx(ctx).Where("level", *req.Level).Scan(&config)
|
||
if err = common.IgnoreScanNoRows(err); err != nil || config == nil {
|
||
return nil, common.NewBadRequestError("等级配置不存在")
|
||
}
|
||
data.Level = *req.Level
|
||
}
|
||
|
||
_, err := dao.TntTenants.Ctx(ctx).Where("id", req.Id).Update(data)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return nil, nil
|
||
}
|
||
|
||
// UpdateTenantChannelScope 更新租户默认渠道范围
|
||
func (s *sAdmin) UpdateTenantChannelScope(ctx context.Context, req *v1.TenantChannelScopeUpdateReq) (*v1.TenantChannelScopeUpdateRes, error) {
|
||
var scopeValue any
|
||
if req.DefaultChannelScope == nil || *req.DefaultChannelScope == "" || *req.DefaultChannelScope == "all" {
|
||
scopeValue = nil
|
||
} else if json.Valid([]byte(*req.DefaultChannelScope)) {
|
||
scopeValue = *req.DefaultChannelScope
|
||
} else {
|
||
return nil, gerror.New("default_channel_scope 必须是有效的 JSON(如 [1,5,12])或 null")
|
||
}
|
||
|
||
_, err := dao.TntTenants.Ctx(ctx).Where("id", req.Id).Data(do.TntTenants{
|
||
DefaultChannelScope: scopeValue,
|
||
}).Update()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return nil, nil
|
||
}
|
||
|
||
// ExportTenants exports tenant list to CSV or Excel.
|
||
func (s *sAdmin) ExportTenants(ctx context.Context, req *v1.TenantExportReq) (*v1.TenantExportRes, error) {
|
||
columns := []export.Column{
|
||
{Field: "id", Header: "ID"},
|
||
{Field: "name", Header: "名称"},
|
||
{Field: "code", Header: "代码"},
|
||
{Field: "owner_name", Header: "所有者"},
|
||
{Field: "status", Header: "状态"},
|
||
{Field: "member_count", Header: "成员数"},
|
||
{Field: "wallet_balance", Header: "钱包余额"},
|
||
{Field: "created_at", Header: "创建时间"},
|
||
}
|
||
|
||
config := export.Config{
|
||
Format: req.Format,
|
||
Filename: "租户_" + gtime.Now().Format("Ymd_His"),
|
||
Columns: columns,
|
||
}
|
||
|
||
return nil, export.GenericExport(ctx, config, func(yield func(map[string]any) bool) {
|
||
offset := 0
|
||
for {
|
||
m := dao.TntTenants.Ctx(ctx)
|
||
if req.Keyword != "" {
|
||
keyword := "%" + strings.TrimSpace(req.Keyword) + "%"
|
||
m = m.Where("name LIKE ? OR code LIKE ?", keyword, keyword)
|
||
}
|
||
if req.Status != "" {
|
||
m = m.Where("status", req.Status)
|
||
}
|
||
var batch []struct {
|
||
Id int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
Code string `json:"code"`
|
||
OwnerUserID int64 `json:"owner_user_id"`
|
||
Status string `json:"status"`
|
||
CreatedAt *gtime.Time `json:"created_at"`
|
||
}
|
||
if err := m.Fields("id, name, code, owner_user_id, status, created_at").OrderDesc("id").Limit(1000).Offset(offset).Scan(&batch); err != nil {
|
||
return
|
||
}
|
||
|
||
// 批量查询本页的 owner 名称 / 成员数 / 钱包余额(避免逐行 N+1)
|
||
tenantIDs := make([]int64, len(batch))
|
||
ownerIDs := make([]int64, len(batch))
|
||
for i, t := range batch {
|
||
tenantIDs[i] = t.Id
|
||
ownerIDs[i] = t.OwnerUserID
|
||
}
|
||
ownerNameMap, memberCountMap, walletBalanceMap := batchTenantAggregates(ctx, tenantIDs, ownerIDs)
|
||
|
||
for _, t := range batch {
|
||
walletBalance := "0"
|
||
if bal, ok := walletBalanceMap[t.Id]; ok && bal != "" {
|
||
walletBalance = bal
|
||
}
|
||
row := map[string]any{
|
||
"id": t.Id,
|
||
"name": t.Name,
|
||
"code": t.Code,
|
||
"owner_name": ownerNameMap[t.OwnerUserID],
|
||
"status": t.Status,
|
||
"member_count": memberCountMap[t.Id],
|
||
"wallet_balance": walletBalance,
|
||
"created_at": t.CreatedAt.String(),
|
||
}
|
||
if !yield(row) {
|
||
return
|
||
}
|
||
}
|
||
if len(batch) < 1000 {
|
||
break
|
||
}
|
||
offset += 1000
|
||
}
|
||
})
|
||
}
|
||
|
||
// TenantApiKeySelect 租户 API Key 下拉选择列表(轻量,不返回密钥原值)。
|
||
// 供渠道调试目标过滤等管理端选择器使用;user_id > 0 时按创建者过滤。
|
||
// 注意:Fields 不能挂在 Count 之前的模型上——gdb 会生成 COUNT(col1, col2, ...),
|
||
// PostgreSQL 的 count 只接受单参数,会直接报错。
|
||
func (s *sAdmin) TenantApiKeySelect(ctx context.Context, req *v1.TenantApiKeySelectReq) (*v1.TenantApiKeySelectRes, error) {
|
||
m := dao.ApiKeys.Ctx(ctx).
|
||
Where("tenant_id", req.TenantID).
|
||
OrderDesc("id")
|
||
if req.UserID > 0 {
|
||
m = m.Where("user_id", req.UserID)
|
||
}
|
||
|
||
total, err := m.Count()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var keys []struct {
|
||
ID int64 `json:"id"`
|
||
Name string `json:"name"`
|
||
KeyPrefix string `json:"key_prefix"`
|
||
UserID int64 `json:"user_id"`
|
||
Status string `json:"status"`
|
||
}
|
||
if err := m.Fields("id, name, key_prefix, user_id, status").Page(req.Page, req.PageSize).Scan(&keys); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
list := make([]v1.TenantApiKeySelectItem, len(keys))
|
||
for i, k := range keys {
|
||
list[i] = v1.TenantApiKeySelectItem{
|
||
ID: k.ID,
|
||
Name: k.Name,
|
||
KeyPrefix: k.KeyPrefix,
|
||
UserID: k.UserID,
|
||
Status: k.Status,
|
||
}
|
||
}
|
||
return &v1.TenantApiKeySelectRes{List: list, Total: total, Page: req.Page, PageSize: req.PageSize}, nil
|
||
}
|