Files
im_manager/server/internal/logic/sys/im_tenant.go
T

445 lines
15 KiB
Go

// Package sys
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2026 HotGo
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
type sSysImTenant struct{}
const (
tenantConfigAliyunModerationEnabled = "aliyun_moderation_enabled"
tenantConfigLocalModerationEnabled = "local_moderation_enabled"
)
type imTenantConfigModeration struct {
Id int64 `json:"id"`
TenantId int64 `json:"tenant_id"`
MaxUsers uint64 `json:"max_users"`
MaxGroups uint64 `json:"max_groups"`
MaxGroupMembers uint `json:"max_group_members"`
AliyunModerationEnabled int `json:"aliyun_moderation_enabled"`
LocalModerationEnabled int `json:"local_moderation_enabled"`
}
func NewSysImTenant() *sSysImTenant {
return &sSysImTenant{}
}
func init() {
service.RegisterSysImTenant(NewSysImTenant())
}
// Model IM租户ORM模型
func (s *sSysImTenant) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
return handler.Model(imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx), option...)
}
func (s *sSysImTenant) configModel(ctx context.Context) *gdb.Model {
return imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx)
}
// List 获取IM租户列表
func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (list []*sysin.ImTenantListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
mod := s.Model(ctx).Fields(
dao.ImTenant.Columns().Id,
dao.ImTenant.Columns().Code,
dao.ImTenant.Columns().Name,
dao.ImTenant.Columns().TenantDesc,
dao.ImTenant.Columns().DbName,
dao.ImTenant.Columns().State,
dao.ImTenant.Columns().CreatedBy,
dao.ImTenant.Columns().UpdatedBy,
dao.ImTenant.Columns().CreatedAt,
dao.ImTenant.Columns().UpdatedAt,
)
if in.Id > 0 {
mod = mod.Where(dao.ImTenant.Columns().Id, in.Id)
}
if in.Code != "" {
mod = mod.WhereLike(dao.ImTenant.Columns().Code, "%"+in.Code+"%")
}
if in.Name != "" {
mod = mod.WhereLike(dao.ImTenant.Columns().Name, "%"+in.Name+"%")
}
if in.DbName != "" {
mod = mod.WhereLike(dao.ImTenant.Columns().DbName, "%"+in.DbName+"%")
}
if in.State != nil {
mod = mod.Where(dao.ImTenant.Columns().State, *in.State)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.ImTenant.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
mod = mod.Page(in.Page, in.PerPage).
OrderDesc(dao.ImTenant.Columns().Id).
Hook(hook.MemberSummary)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM租户列表失败,请稍后重试!")
return
}
if len(list) == 0 {
return
}
if err = s.fillListConfig(ctx, list); err != nil {
return
}
return
}
func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenantListModel) (err error) {
ids := make([]int64, 0, len(list))
for _, item := range list {
ids = append(ids, item.Id)
}
var configs []*imTenantConfigModeration
if err = s.configModel(ctx).
WhereIn(dao.ImTenantConfig.Columns().TenantId, ids).
Scan(&configs); err != nil {
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
}
configMap := make(map[int64]*imTenantConfigModeration, len(configs))
for _, item := range configs {
configMap[item.TenantId] = item
}
var tenants []*entity.ImTenant
if err = s.Model(ctx).
Fields(dao.ImTenant.Columns().Id, dao.ImTenant.Columns().DbDsn).
WhereIn(dao.ImTenant.Columns().Id, ids).
Scan(&tenants); err != nil {
return gerror.Wrap(err, "获取IM租户连接配置失败,请稍后重试!")
}
dsnMap := make(map[int64]string, len(tenants))
for _, item := range tenants {
dsnMap[item.Id] = item.DbDsn
}
for _, item := range list {
if conf, ok := configMap[item.Id]; ok {
item.MaxUsers = conf.MaxUsers
item.MaxGroups = conf.MaxGroups
item.MaxGroupMembers = conf.MaxGroupMembers
item.AliyunModerationEnabled = conf.AliyunModerationEnabled
item.LocalModerationEnabled = conf.LocalModerationEnabled
}
item.DbDsnConfigured = dsnMap[item.Id] != ""
}
return
}
// Edit 修改/新增IM租户
func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
tenantID := in.Id
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
if err = s.checkCodeUnique(ctx, in.Id, in.Code); err != nil {
return
}
userId := contexts.GetUserId(ctx)
if in.Id > 0 {
in.UpdatedBy = userId
if _, err = s.Model(ctx).
Fields(sysin.ImTenantUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
return gerror.Wrap(err, "修改IM租户失败,请稍后重试!")
}
return s.saveConfig(ctx, in.Id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, in.AliyunModerationEnabled, in.LocalModerationEnabled, userId)
}
in.CreatedBy = userId
in.UpdatedBy = userId
id, err := s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(sysin.ImTenantInsertFields{}).
Data(in).InsertAndGetId()
if err != nil {
return gerror.Wrap(err, "新增IM租户失败,请稍后重试!")
}
tenantID = id
return s.saveConfig(ctx, id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, in.AliyunModerationEnabled, in.LocalModerationEnabled, userId)
})
if err != nil {
return err
}
_ = publishImPolicyEvent(ctx, tenantID, in.Code, "tenant.features.updated", "moderation", 0)
return nil
}
func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code string) (err error) {
mod := s.Model(ctx).Where(dao.ImTenant.Columns().Code, code)
if id > 0 {
mod = mod.Where(dao.ImTenant.Columns().Id+"<>?", id)
}
count, err := mod.Count()
if err != nil {
return gerror.Wrap(err, "检查IM租户编码失败,请稍后重试!")
}
if count > 0 {
return gerror.New("租户编码已存在")
}
return
}
func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers uint64, maxGroups uint64, maxGroupMembers uint, aliyunModerationEnabled, localModerationEnabled int, userId int64) (err error) {
var conf *entity.ImTenantConfig
if err = s.configModel(ctx).
Where(dao.ImTenantConfig.Columns().TenantId, tenantId).
Scan(&conf); err != nil {
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
}
data := g.Map{
dao.ImTenantConfig.Columns().MaxUsers: maxUsers,
dao.ImTenantConfig.Columns().MaxGroups: maxGroups,
dao.ImTenantConfig.Columns().MaxGroupMembers: maxGroupMembers,
tenantConfigAliyunModerationEnabled: aliyunModerationEnabled,
tenantConfigLocalModerationEnabled: localModerationEnabled,
}
if conf != nil && conf.Id > 0 {
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
if _, err = s.configModel(ctx).
Fields(
dao.ImTenantConfig.Columns().MaxUsers,
dao.ImTenantConfig.Columns().MaxGroups,
dao.ImTenantConfig.Columns().MaxGroupMembers,
tenantConfigAliyunModerationEnabled,
tenantConfigLocalModerationEnabled,
dao.ImTenantConfig.Columns().UpdatedBy,
).
WherePri(conf.Id).Data(data).Update(); err != nil {
return gerror.Wrap(err, "修改IM租户配置失败,请稍后重试!")
}
return
}
data[dao.ImTenantConfig.Columns().TenantId] = tenantId
data[dao.ImTenantConfig.Columns().CreatedBy] = userId
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
if _, err = s.configModel(ctx).
Fields(
dao.ImTenantConfig.Columns().TenantId,
dao.ImTenantConfig.Columns().MaxUsers,
dao.ImTenantConfig.Columns().MaxGroups,
dao.ImTenantConfig.Columns().MaxGroupMembers,
tenantConfigAliyunModerationEnabled,
tenantConfigLocalModerationEnabled,
dao.ImTenantConfig.Columns().CreatedBy,
dao.ImTenantConfig.Columns().UpdatedBy,
).
Data(data).Insert(); err != nil {
return gerror.Wrap(err, "新增IM租户配置失败,请稍后重试!")
}
return
}
// View 获取IM租户指定信息
func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res *sysin.ImTenantViewModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取IM租户信息失败,请稍后重试!")
return
}
if res == nil {
return
}
var conf *imTenantConfigModeration
if err = s.configModel(ctx).
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
Scan(&conf); err != nil {
err = gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
return
}
if conf != nil {
res.MaxUsers = conf.MaxUsers
res.MaxGroups = conf.MaxGroups
res.MaxGroupMembers = conf.MaxGroupMembers
res.AliyunModerationEnabled = conf.AliyunModerationEnabled
res.LocalModerationEnabled = conf.LocalModerationEnabled
}
res.DbDsnConfigured = res.DbDsn != ""
return
}
// Status 更新IM租户状态
func (s *sSysImTenant) Status(ctx context.Context, in *sysin.ImTenantStatusInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.ImTenant.Columns().State: in.State,
dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
err = gerror.Wrap(err, "更新IM租户状态失败,请稍后重试!")
return
}
return
}
func (s *sSysImTenant) Usage(ctx context.Context, in *sysin.ImTenantUsageInp) (res *sysin.ImTenantUsageModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
var tenant *entity.ImTenant
if err = s.Model(ctx).WherePri(in.Id).Scan(&tenant); err != nil {
return nil, gerror.Wrap(err, "获取IM租户信息失败,请稍后重试!")
}
if tenant == nil || tenant.Id <= 0 {
return nil, gerror.New("IM租户不存在")
}
res = &sysin.ImTenantUsageModel{
Id: tenant.Id,
Code: tenant.Code,
Name: tenant.Name,
DbName: tenant.DbName,
DbDsnConfigured: tenant.DbDsn != "",
Warnings: []string{},
}
db, _, dbErr := imtenant.DB(ctx, tenant.Id)
if dbErr != nil {
res.DbReachable = false
res.DbCheckError = dbErr.Error()
return res, nil
}
if _, pingErr := db.GetValue(ctx, "SELECT 1"); pingErr != nil {
res.DbReachable = false
res.DbCheckError = pingErr.Error()
return res, nil
}
res.DbReachable = true
res.SchemaVersion = s.queryScalarString(ctx, db, "kvmeta.version", "SELECT value FROM kvmeta WHERE key_='version' LIMIT 1", &res.Warnings)
res.UserCount = s.queryScalarInt64(ctx, db, "im_users.total", "SELECT COUNT(*) FROM im_users", &res.Warnings)
res.ActiveUserCount = s.queryScalarInt64(ctx, db, "im_users.active", "SELECT COUNT(*) FROM im_users WHERE state=0", &res.Warnings)
res.GroupCount = s.queryScalarInt64(ctx, db, "im_group_settings", "SELECT COUNT(*) FROM im_group_settings WHERE group_status<>'dissolved'", &res.Warnings)
res.DeviceCount = s.queryScalarInt64(ctx, db, "im_devices", "SELECT COUNT(*) FROM im_devices", &res.Warnings)
res.PushTokenCount = s.queryScalarInt64(ctx, db, "im_device_push_tokens", "SELECT COUNT(*) FROM im_device_push_tokens", &res.Warnings)
res.InviteUseCount = s.queryScalarInt64(ctx, db, "im_registration_invite_uses", "SELECT COUNT(*) FROM im_registration_invite_uses", &res.Warnings)
res.OutboxPendingCount = s.queryScalarInt64(ctx, db, "im_business_events_outbox", "SELECT COUNT(*) FROM im_business_events_outbox WHERE status='pending'", &res.Warnings)
return
}
func (s *sSysImTenant) queryScalarInt64(ctx context.Context, db gdb.DB, label, sql string, warnings *[]string) int64 {
value, err := db.GetValue(ctx, sql)
if err != nil {
*warnings = append(*warnings, label+": "+err.Error())
return 0
}
return value.Int64()
}
func (s *sSysImTenant) queryScalarString(ctx context.Context, db gdb.DB, label, sql string, warnings *[]string) string {
value, err := db.GetValue(ctx, sql)
if err != nil {
*warnings = append(*warnings, label+": "+err.Error())
return ""
}
return value.String()
}
func (s *sSysImTenant) DeletionCheck(ctx context.Context, in *sysin.ImTenantDeletionCheckInp) (res *sysin.ImTenantDeletionCheckModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
usage, err := s.Usage(ctx, &sysin.ImTenantUsageInp{Id: in.Id})
if err != nil {
return nil, err
}
res = &sysin.ImTenantDeletionCheckModel{
Id: usage.Id,
Code: usage.Code,
Name: usage.Name,
Blockers: []string{},
Warnings: usage.Warnings,
UserCount: usage.UserCount,
GroupCount: usage.GroupCount,
PendingOutbox: usage.OutboxPendingCount,
}
if !usage.DbReachable {
res.Blockers = append(res.Blockers, "租户业务库不可达:"+usage.DbCheckError)
}
if usage.UserCount > 0 {
res.Blockers = append(res.Blockers, fmt.Sprintf("租户仍有 %d 个用户", usage.UserCount))
}
if usage.GroupCount > 0 {
res.Blockers = append(res.Blockers, fmt.Sprintf("租户仍有 %d 个群", usage.GroupCount))
}
if usage.OutboxPendingCount > 0 {
res.Blockers = append(res.Blockers, fmt.Sprintf("租户仍有 %d 条待处理 Outbox", usage.OutboxPendingCount))
}
res.CanEnterDeleting = len(res.Blockers) == 0
return
}
func (s *sSysImTenant) EnterDeleting(ctx context.Context, in *sysin.ImTenantLifecycleInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
check, err := s.DeletionCheck(ctx, &sysin.ImTenantDeletionCheckInp{Id: in.Id})
if err != nil {
return err
}
if !check.CanEnterDeleting {
return gerror.New("租户未通过删除前检查,不能进入删除流程")
}
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.ImTenant.Columns().State: consts.ImTenantStateDeleting,
dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
return gerror.Wrap(err, "设置IM租户删除流程失败,请稍后重试!")
}
return
}
func (s *sSysImTenant) Restore(ctx context.Context, in *sysin.ImTenantLifecycleInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
if _, err = s.Model(ctx).
WherePri(in.Id).
WhereIn(dao.ImTenant.Columns().State, []int{consts.ImTenantStateSuspended, consts.ImTenantStateDeleting}).
Data(g.Map{
dao.ImTenant.Columns().State: consts.ImTenantStateActive,
dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
return gerror.Wrap(err, "恢复IM租户失败,请稍后重试!")
}
return
}