Files
team-api/internal/logic/admin/tenant.go
T

549 lines
15 KiB
Go

package admin
import (
"context"
"encoding/json"
"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/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 != 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))
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
}
maxMembers := 10
if req.MaxMembers != nil && *req.MaxMembers >= 1 {
maxMembers = *req.MaxMembers
}
maxConcurrency := 0
if req.MaxConcurrency != nil {
maxConcurrency = *req.MaxConcurrency
}
var tenantID int64
err = dao.TntTenants.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
tenantResult, err := tx.Model("tnt_tenants").Ctx(ctx).Data(do.TntTenants{
Name: strings.TrimSpace(req.TenantName),
Code: tenantCode,
MaxMembers: maxMembers,
MaxConcurrency: maxConcurrency,
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 := tx.Model("tnt_users").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 = tx.Model("tnt_tenants").Ctx(ctx).
Where("id", tenantID).
Data(do.TntTenants{
OwnerUserId: ownerUserID,
}).Update()
if err != nil {
return gerror.Wrapf(err, "set tenant owner")
}
_, err = tx.Model("bil_wallets").Ctx(ctx).Data(do.BilWallets{
TenantId: tenantID,
Balance: 0,
FrozenBalance: 0,
Currency: "CNY",
}).Insert()
if err != nil {
return gerror.Wrapf(err, "create wallet")
}
return nil
})
if err != nil {
return nil, err
}
return &v1.TenantCreateRes{Id: tenantID}, nil
}
// 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"`
CreatedAt *gtime.Time `json:"created_at"`
UpdatedAt *gtime.Time `json:"updated_at"`
}
err = m.OrderDesc("id").
Page(page, pageSize).
Scan(&tenants)
if err != nil {
return nil, err
}
items := make([]v1.TenantItem, len(tenants))
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,
CreatedAt: t.CreatedAt.String(),
UpdatedAt: t.UpdatedAt.String(),
}
// Get owner name
var owner *struct {
DisplayName string `json:"display_name"`
}
_ = dao.TntUsers.Ctx(ctx).
Where("id", t.OwnerUserID).Scan(&owner)
if owner != nil {
item.OwnerName = owner.DisplayName
}
// Get member count
memberCount, _ := dao.TntUsers.Ctx(ctx).
Where("tenant_id", t.Id).Count()
item.MemberCount = memberCount
// Get wallet balance
var wallet *struct {
Balance string `json:"balance"`
}
_ = dao.BilWallets.Ctx(ctx).
Where("tenant_id", t.Id).Scan(&wallet)
if wallet != nil && wallet.Balance != "" {
item.WalletBalance = wallet.Balance
} 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 != 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
var wallet *struct {
Balance string `json:"balance"`
}
_ = dao.BilWallets.Ctx(ctx).
Where("tenant_id", req.Id).Scan(&wallet)
walletBalance := "0"
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.Id > 0 {
levelName = levelConfig.Name
}
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,
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 != 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 != "" {
data.Name = req.Name
}
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
}
// 管理员手动调整等级:同步更新 max_members 和 max_concurrency
if req.Level != nil {
data.Level = *req.Level
var config entity.TntTenantLevelConfigs
err := dao.TntTenantLevelConfigs.Ctx(ctx).Where("level", *req.Level).Scan(&config)
if err != nil {
return nil, common.NewBadRequestError("等级配置不存在")
}
if config.Id > 0 {
data.MaxMembers = config.MaxMembers
data.MaxConcurrency = config.MaxConcurrency
}
}
_, 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(g.Map{
"default_channel_scope": 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,
}
fetchTenantRow := func(t 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"`
}) map[string]any {
var owner *struct {
DisplayName string `json:"display_name"`
}
_ = dao.TntUsers.Ctx(ctx).Where("id", t.OwnerUserID).Scan(&owner)
memberCount, _ := dao.TntUsers.Ctx(ctx).Where("tenant_id", t.Id).Count()
var wallet *struct {
Balance string `json:"balance"`
}
_ = dao.BilWallets.Ctx(ctx).Where("tenant_id", t.Id).Scan(&wallet)
walletBalance := "0"
if wallet != nil && wallet.Balance != "" {
walletBalance = wallet.Balance
}
ownerName := ""
if owner != nil {
ownerName = owner.DisplayName
}
return map[string]any{
"id": t.Id,
"name": t.Name,
"code": t.Code,
"owner_name": ownerName,
"status": t.Status,
"member_count": memberCount,
"wallet_balance": walletBalance,
"created_at": t.CreatedAt.String(),
}
}
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
}
for _, t := range batch {
if !yield(fetchTenantRow(t)) {
return
}
}
if len(batch) < 1000 {
break
}
offset += 1000
}
})
}