// 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/adminin" "hotgo/internal/model/input/sysin" "hotgo/internal/service" "hotgo/utility/tree" "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" ) type sSysImTenant struct{} // imManageRoleKey 商户级IM管理员角色标识 const imManageRoleKey = "imManage" 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...) } // 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().MaxUsers, dao.ImTenant.Columns().MaxGroups, dao.ImTenant.Columns().MaxGroupMembers, dao.ImTenant.Columns().AliyunModerationEnabled, dao.ImTenant.Columns().LocalModerationEnabled, dao.ImTenant.Columns().MessageEncryptionEnabled, dao.ImTenant.Columns().RealnameReviewPolicy, dao.ImTenant.Columns().State, dao.ImTenant.Columns().CreatedBy, dao.ImTenant.Columns().UpdatedBy, dao.ImTenant.Columns().CreatedAt, dao.ImTenant.Columns().UpdatedAt, ) if !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) { tenantId := contexts.GetTenantId(ctx) if tenantId <= 0 { err = gerror.New("当前管理员未关联IM租户") return } mod = mod.Where(dao.ImTenant.Columns().Id, tenantId) } 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.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]) } orderDesc := false for _, sorter := range in.GetSorters() { if sorter != nil && sorter.ColumnKey == "id" { orderDesc = sorter.Order == "descend" break } } mod = mod.Page(in.Page, in.PerPage) if orderDesc { mod = mod.OrderDesc(dao.ImTenant.Columns().Id) } else { mod = mod.OrderAsc(dao.ImTenant.Columns().Id) } mod = mod.Hook(hook.MemberSummary) if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { err = gerror.Wrap(err, "获取IM租户列表失败,请稍后重试!") return } return } // Add 新增IM租户 func (s *sSysImTenant) Add(ctx context.Context, in *sysin.ImTenantEditInp) (err error) { if err = in.Filter(ctx); err != nil { return } if !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) { return gerror.New("只有平台管理员可以新增商户") } in.Id = 0 return s.saveTenant(ctx, in) } // Edit 修改IM租户 func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err error) { if err = in.Filter(ctx); err != nil { return } if in.Id <= 0 { return gerror.New("商户ID不能为空") } if !isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) { tenantId := contexts.GetTenantId(ctx) if tenantId <= 0 { return gerror.New("当前管理员未关联IM租户") } if in.Id != tenantId { return gerror.New("无权编辑其它商户") } } return s.saveTenant(ctx, in) } // isSuperTenantAdmin 当前操作者是否为admin超管 func isSuperTenantAdmin(ctx context.Context) bool { return service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) } func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp) (err error) { super := isSuperTenantAdmin(ctx) isCreate := in.Id == 0 tenantID := in.Id requestedState := in.State if isCreate { if requestedState != consts.ImTenantStateActive && requestedState != consts.ImTenantStateSuspended { return gerror.New("新商户创建完成后的状态只能是正常或已停用") } if err = ensureTenantIDSequence(ctx); err != nil { return err } in.State = consts.ImTenantStateProvisioning } if err = validateTenantMessageEncryption(ctx, in.Id, in.MessageEncryptionEnabled); err != nil { return err } 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 data := g.Map{ dao.ImTenant.Columns().Code: in.Code, dao.ImTenant.Columns().Name: in.Name, dao.ImTenant.Columns().TenantDesc: in.TenantDesc, dao.ImTenant.Columns().MaxUsers: in.MaxUsers, dao.ImTenant.Columns().MaxGroups: in.MaxGroups, dao.ImTenant.Columns().MaxGroupMembers: in.MaxGroupMembers, dao.ImTenant.Columns().AliyunModerationEnabled: in.AliyunModerationEnabled, dao.ImTenant.Columns().LocalModerationEnabled: in.LocalModerationEnabled, dao.ImTenant.Columns().MessageEncryptionEnabled: in.MessageEncryptionEnabled, dao.ImTenant.Columns().RealnameReviewPolicy: in.RealnameReviewPolicy, dao.ImTenant.Columns().State: in.State, dao.ImTenant.Columns().UpdatedBy: userId, } if _, err = s.Model(ctx). WherePri(in.Id).Data(data).Update(); err != nil { return gerror.Wrap(err, "修改IM租户失败,请稍后重试!") } return nil } in.Id, err = nextTenantID(tx) if err != nil { return err } in.CreatedBy = userId in.UpdatedBy = userId _, err = tx.Model(dao.ImTenant.Table()).Safe().Ctx(ctx). Fields(sysin.ImTenantInsertFields{}). Data(in).Insert() if err != nil { return gerror.Wrap(err, "新增IM租户失败,请稍后重试!") } tenantID = in.Id // 新建商户时复制安全策略模板为本商户初始策略(保存即生效模型) if err = copyDefaultPolicyForTenant(ctx, tx, in.Id, userId); err != nil { return err } return nil }) if err != nil { return err } // 新增商户时自动创建绑定的后台管理员账号,失败则补偿回滚商户数据 if isCreate { if err = s.createTenantAdminMember(ctx, super, in, tenantID); err != nil { s.rollbackTenantCreation(ctx, tenantID) return err } if err = provisionTenantMessageTables(ctx, tenantID); err != nil { return gerror.Wrap(err, "商户已登记为初始化中,但业务分表创建失败") } if err = initializeTenantWorkbench(ctx, tenantID, in.Code); err != nil { return gerror.Wrap(err, "商户业务分表已创建,但工作台初始化失败") } if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).WherePri(tenantID).Data(g.Map{ dao.ImTenant.Columns().State: requestedState, dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx), }).Update(); err != nil { return gerror.Wrap(err, "商户分表已创建,但更新商户状态失败") } in.State = requestedState } else if in.AdminPassword != "" { // 编辑商户时填写了新密码则重置管理员密码,留空不修改。 if err = s.resetTenantAdminPassword(ctx, tenantID, in.AdminPassword); err != nil { return err } } _ = publishImPolicyEvent(ctx, tenantID, in.Code, "tenant.features.updated", "moderation", 0) return nil } // createTenantAdminMember 为新增商户自动创建后台管理员账号(默认绑定imManage角色) func (s *sSysImTenant) createTenantAdminMember(ctx context.Context, super bool, in *sysin.ImTenantEditInp, tenantId int64) (err error) { var role *entity.AdminRole if err = dao.AdminRole.Ctx(ctx). Fields(dao.AdminRole.Columns().Id). Where(dao.AdminRole.Columns().Key, imManageRoleKey). Where(dao.AdminRole.Columns().Status, consts.StatusEnabled). OrderAsc(dao.AdminRole.Columns().Id). Scan(&role); err != nil { return gerror.Wrap(err, "查询IM管理员角色失败,请稍后重试!") } if role == nil || role.Id <= 0 { return gerror.New("IM管理员角色(imManage)未配置,请联系运维") } // 部门挂载:超管挂公司部门;非超管受部门数据权限限制,只能从自己部门的下级中选取 var deptId int64 if super { companyDeptId, deptErr := dao.AdminDept.Ctx(ctx). Fields(dao.AdminDept.Columns().Id). Where(dao.AdminDept.Columns().Type, consts.DeptTypeCompany). Where(dao.AdminDept.Columns().Status, consts.StatusEnabled). OrderAsc(dao.AdminDept.Columns().Id). Limit(1). Value() if deptErr != nil { return gerror.Wrap(deptErr, "获取公司部门失败,请稍后重试!") } if companyDeptId.IsEmpty() { return gerror.New("请先配置公司部门") } deptId = companyDeptId.Int64() } else { mb := contexts.GetUser(ctx) if mb == nil || mb.DeptId <= 0 { return gerror.New("获取当前用户部门失败") } subDeptId, deptErr := dao.AdminDept.Ctx(ctx). Fields(dao.AdminDept.Columns().Id). Where(dao.AdminDept.Columns().Status, consts.StatusEnabled). WhereNot(dao.AdminDept.Columns().Id, mb.DeptId). WhereLike(dao.AdminDept.Columns().Tree, "%"+tree.GetIdLabel(mb.DeptId)+"%"). OrderAsc(dao.AdminDept.Columns().Id). Limit(1). Value() if deptErr != nil { return gerror.Wrap(deptErr, "获取下级部门失败,请稍后重试!") } if subDeptId.IsEmpty() { return gerror.New("当前部门下没有可用的下级部门来挂载商户管理员账号,请先配置下级部门") } deptId = subDeptId.Int64() } // 透传底层可读错误,便于前端定位 return service.AdminMember().Edit(ctx, &adminin.MemberEditInp{ TenantId: tenantId, RoleId: role.Id, Username: in.AdminUsername, Password: in.AdminPassword, RealName: in.Name + "管理员", DeptId: deptId, Status: consts.StatusEnabled, }) } // resetTenantAdminPassword 重置商户管理员账号的密码(编辑弹窗填写新密码时调用) func (s *sSysImTenant) resetTenantAdminPassword(ctx context.Context, tenantId int64, password string) (err error) { roleVal, err := dao.AdminRole.Ctx(ctx). Fields(dao.AdminRole.Columns().Id). Where(dao.AdminRole.Columns().Key, imManageRoleKey). Where(dao.AdminRole.Columns().Status, consts.StatusEnabled). Limit(1). Value() if err != nil { return gerror.Wrap(err, "查询IM管理员角色失败,请稍后重试!") } if roleVal.IsEmpty() { return gerror.New("IM管理员角色(imManage)未配置,请联系运维") } memberId, err := dao.AdminMember.Ctx(ctx). Fields(dao.AdminMember.Columns().Id). Where(dao.AdminMember.Columns().TenantId, tenantId). Where(dao.AdminMember.Columns().RoleId, roleVal.Int64()). OrderAsc(dao.AdminMember.Columns().Id). Limit(1). Value() if err != nil { return gerror.Wrap(err, "查询商户管理员账号失败,请稍后重试!") } if memberId.IsEmpty() { return gerror.New("该商户暂无管理员账号,无法重置密码") } return service.AdminMember().ResetPwd(ctx, &adminin.MemberResetPwdInp{ Id: memberId.Int64(), Password: password, }) } // rollbackTenantCreation 商户创建补偿:删除已写入的商户及配置记录 func (s *sSysImTenant) rollbackTenantCreation(ctx context.Context, tenantId int64) { _ = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) { if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}). WherePri(tenantId).Delete(); err != nil { return } // 清理建户时复制的安全策略与宵禁规则,避免残留孤儿数据(商户未建成,硬删不留软删垃圾) if _, err = tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx). Where(dao.ImSecurityPolicy.Columns().TenantId, tenantId).Unscoped().Delete(); err != nil { return } _, err = tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx). Where(dao.ImMessageCurfewRules.Columns().TenantId, tenantId).Unscoped().Delete() return }) } // copyDefaultPolicyForTenant 新建商户时复制安全策略模板为本商户自己的初始策略。 // 模板未初始化时跳过(运行时由内部配置接口的默认值兜底),不阻断建户。 func copyDefaultPolicyForTenant(ctx context.Context, tx gdb.TX, tenantID, operator int64) (err error) { tpl, err := loadPolicyTemplateRow(tx.Model(imPolicyTemplateTable).Safe().Ctx(ctx)) if err != nil { return gerror.Wrap(err, "读取安全策略模板失败") } if tpl == nil { return nil } pc := dao.ImSecurityPolicy.Columns() now := gtime.Now() data := g.Map{ pc.TenantId: tenantID, pc.PolicyName: tpl["policy_name"].String(), pc.PolicyVersion: 1, pc.PublishStatus: consts.ImSecurityPublishPublished, pc.PasswordPolicy: tpl["password_policy"].String(), pc.LoginRateLimit: tpl["login_rate_limit"].String(), pc.SensitiveWordPolicy: tpl["sensitive_word_policy"].String(), pc.FileReviewPolicy: tpl["file_review_policy"].String(), pc.RealnamePolicy: tpl["realname_policy"].String(), pc.RiskChallengePolicy: tpl["risk_challenge_policy"].String(), pc.GlobalFriendEnabled: tpl["global_friend_enabled"].Int(), pc.FriendDisabledPolicy: tpl["friend_disabled_policy"].String(), pc.MessageCurfewEnabled: tpl["message_curfew_enabled"].Int(), pc.Remark: "由默认模板复制", pc.PublishedAt: now, pc.PublishedBy: operator, pc.CreatedAt: now, pc.CreatedBy: operator, pc.UpdatedAt: now, pc.UpdatedBy: operator, } result, err := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Data(data).Insert() if err != nil { return gerror.Wrap(err, "复制安全策略模板失败") } policyID, err := result.LastInsertId() if err != nil { return err } // 同步复制模板宵禁规则 rc := dao.ImMessageCurfewRules.Columns() for _, rule := range templateCurfewRules(tpl) { ruleData := g.Map{ rc.TenantId: tenantID, rc.PolicyId: policyID, rc.RuleCode: rule.RuleCode, rc.RuleName: rule.RuleName, rc.DateType: rule.DateType, rc.CustomDates: policyJSON(rule.CustomDates, "[]"), rc.Weekdays: policyJSON(rule.Weekdays, "[]"), rc.Timezone: rule.Timezone, rc.StartTime: rule.StartTime, rc.EndTime: rule.EndTime, rc.Scope: rule.Scope, rc.WhitelistUserIds: policyJSON(rule.WhitelistUserIds, "[]"), rc.WhitelistGroupIds: policyJSON(rule.WhitelistGroupIds, "[]"), rc.Message: rule.Message, rc.Status: rule.Status, rc.Sort: rule.Sort, rc.CreatedAt: now, rc.CreatedBy: operator, rc.UpdatedAt: now, rc.UpdatedBy: operator, } if _, err = tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).Data(ruleData).Insert(); err != nil { return gerror.Wrap(err, "复制模板宵禁规则失败") } } 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 validateTenantMessageEncryption(ctx context.Context, tenantId int64, enabled int) error { if enabled == 1 { count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx). Where("tenant_id", tenantId). Where("status", consts.ImMessageKeyStatusActive). Count() if queryErr != nil { return gerror.Wrap(queryErr, "检查商户消息密钥失败") } if count == 0 { return gerror.New("启用消息加密前必须先在消息密钥管理中初始化有效密钥") } } return nil } // 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 = ensureImTenantTargetAllowed(ctx, in.Id); 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 } // 查询商户自动创建的管理员账号(编辑弹窗展示) roleVal, roleErr := dao.AdminRole.Ctx(ctx). Fields(dao.AdminRole.Columns().Id). Where(dao.AdminRole.Columns().Key, imManageRoleKey). Where(dao.AdminRole.Columns().Status, consts.StatusEnabled). Limit(1). Value() if roleErr == nil && !roleVal.IsEmpty() { userVal, userErr := dao.AdminMember.Ctx(ctx). Fields(dao.AdminMember.Columns().Username). Where(dao.AdminMember.Columns().TenantId, in.Id). Where(dao.AdminMember.Columns().RoleId, roleVal.Int64()). OrderAsc(dao.AdminMember.Columns().Id). Limit(1). Value() if userErr == nil && !userVal.IsEmpty() { res.AdminUsername = userVal.String() } } return } // Status 更新IM租户状态 func (s *sSysImTenant) Status(ctx context.Context, in *sysin.ImTenantStatusInp) (err error) { if err = in.Filter(ctx); err != nil { return } if in.State == consts.ImTenantStateActive { if err = provisionTenantMessageTables(ctx, in.Id); err != nil { return gerror.Wrap(err, "启用商户前创建业务分表失败") } if err = initializeTenantWorkbench(ctx, in.Id, ""); err != nil { return gerror.Wrap(err, "启用商户前初始化工作台失败") } } 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 } if err = ensureImTenantTargetAllowed(ctx, in.Id); 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, 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) tenantId := tenant.Id res.UserCount = s.queryScalarInt64(ctx, db, "im_users.total", "SELECT COUNT(*) FROM im_users WHERE tenant_id=?", &res.Warnings, tenantId) res.ActiveUserCount = s.queryScalarInt64(ctx, db, "im_users.active", "SELECT COUNT(*) FROM im_users WHERE tenant_id=? AND state=0", &res.Warnings, tenantId) res.GroupCount = s.queryScalarInt64(ctx, db, "im_group_settings", "SELECT COUNT(*) FROM im_group_settings WHERE tenant_id=? AND group_status<>'dissolved'", &res.Warnings, tenantId) res.DeviceCount = s.queryScalarInt64(ctx, db, "im_devices", "SELECT COUNT(*) FROM im_devices WHERE tenant_id=?", &res.Warnings, tenantId) res.PushTokenCount = s.queryScalarInt64(ctx, db, "im_device_push_tokens", "SELECT COUNT(*) FROM im_device_push_tokens WHERE tenant_id=?", &res.Warnings, tenantId) res.InviteUseCount = s.queryScalarInt64(ctx, db, "im_registration_invite_uses", "SELECT COUNT(*) FROM im_registration_invite_uses WHERE tenant_id=?", &res.Warnings, tenantId) res.OutboxPendingCount = s.queryScalarInt64(ctx, db, "im_business_events_outbox", "SELECT COUNT(*) FROM im_business_events_outbox WHERE tenant_id=? AND status='pending'", &res.Warnings, tenantId) return } func ensureImTenantTargetAllowed(ctx context.Context, tenantId int64) error { if isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) { return nil } if tenantId <= 0 { return gerror.New("商户ID不能为空") } boundTenantId := contexts.GetTenantId(ctx) if boundTenantId <= 0 { return gerror.New("当前管理员未关联IM租户") } if tenantId != boundTenantId { return gerror.New("无权访问其它商户") } return nil } func (s *sSysImTenant) queryScalarInt64(ctx context.Context, db gdb.DB, label, sql string, warnings *[]string, args ...any) int64 { value, err := db.GetValue(ctx, sql, args...) 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 = provisionTenantMessageTables(ctx, in.Id); err != nil { return gerror.Wrap(err, "恢复商户前创建业务分表失败") } if err = initializeTenantWorkbench(ctx, in.Id, ""); err != nil { return gerror.Wrap(err, "恢复商户前初始化工作台失败") } 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 }