diff --git a/server/internal/library/imtenant/db.go b/server/internal/library/imtenant/db.go index 8d85d39..04a8b7a 100644 --- a/server/internal/library/imtenant/db.go +++ b/server/internal/library/imtenant/db.go @@ -95,7 +95,12 @@ func tenantLink(ctx context.Context, tenant *entity.ImTenant) (string, error) { return normalizeMySQLTimeOptions(tenant.DbDsn) } if tenant.DbName == "" { - return "", gerror.New("租户业务库未配置") + // 未配置独立业务库的商户回落共享默认库 + defaultLink := defaultDbLink(ctx) + if defaultLink == "" { + return "", gerror.New("默认数据库连接未配置,无法回落共享业务库") + } + return normalizeMySQLTimeOptions(defaultLink) } defaultLink := defaultDbLink(ctx) diff --git a/server/internal/logic/sys/im_tenant.go b/server/internal/logic/sys/im_tenant.go index 6479886..1745e39 100644 --- a/server/internal/logic/sys/im_tenant.go +++ b/server/internal/logic/sys/im_tenant.go @@ -15,8 +15,10 @@ import ( "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" @@ -32,6 +34,9 @@ const ( tenantConfigRealnameReviewPolicy = "realname_review_policy" ) +// imManageRoleKey 商户级IM管理员角色标识 +const imManageRoleKey = "imManage" + type imTenantConfigModeration struct { Id int64 `json:"id" orm:"id"` TenantId int64 `json:"tenant_id" orm:"tenant_id"` @@ -97,7 +102,7 @@ func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (lis if in.Name != "" { mod = mod.WhereLike(dao.ImTenant.Columns().Name, "%"+in.Name+"%") } - if in.DbName != "" { + if in.DbName != "" && isSuperTenantAdmin(ctx) { mod = mod.WhereLike(dao.ImTenant.Columns().DbName, "%"+in.DbName+"%") } if in.State != nil { @@ -122,6 +127,12 @@ func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (lis if err = s.fillListConfig(ctx, list); err != nil { return } + // 业务库名仅超管可见,非超管脱敏返回 + if !isSuperTenantAdmin(ctx) { + for _, item := range list { + item.DbName = "" + } + } return } @@ -201,8 +212,22 @@ func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err 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 + + // 业务库名与独立连接串仅超管可维护,非超管提交一律强制忽略 + if !super { + in.DbName = "" + in.DbDsn = "" + } + 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 @@ -211,9 +236,19 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp 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().State: in.State, + dao.ImTenant.Columns().UpdatedBy: userId, + } + if super { + data[dao.ImTenant.Columns().DbName] = in.DbName + data[dao.ImTenant.Columns().DbDsn] = in.DbDsn + } if _, err = s.Model(ctx). - Fields(sysin.ImTenantUpdateFields{}). - WherePri(in.Id).Data(in).Update(); err != nil { + WherePri(in.Id).Data(data).Update(); err != nil { return gerror.Wrap(err, "修改IM租户失败,请稍后重试!") } return s.saveConfig(ctx, in, userId) @@ -234,10 +269,98 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp if err != nil { return err } + + // 新增商户时自动创建绑定的后台管理员账号,失败则补偿回滚商户数据 + if isCreate { + if err = s.createTenantAdminMember(ctx, super, in, tenantID); err != nil { + s.rollbackTenantCreation(ctx, tenantID) + 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, + }) +} + +// 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 + } + _, err = s.configModel(ctx). + Where(dao.ImTenantConfig.Columns().TenantId, tenantId).Delete() + return + }) +} + 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 { @@ -353,6 +476,11 @@ func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res res.RealnameReviewPolicy = conf.RealnameReviewPolicy } res.DbDsnConfigured = res.DbDsn != "" + // 业务库名与独立连接串仅超管可见,非超管脱敏返回(保留是否已配置标记) + if !isSuperTenantAdmin(ctx) { + res.DbName = "" + res.DbDsn = "" + } return } @@ -392,6 +520,10 @@ func (s *sSysImTenant) Usage(ctx context.Context, in *sysin.ImTenantUsageInp) (r DbDsnConfigured: tenant.DbDsn != "", Warnings: []string{}, } + // 业务库名仅超管可见,非超管脱敏返回 + if !isSuperTenantAdmin(ctx) { + res.DbName = "" + } db, _, dbErr := imtenant.DB(ctx, tenant.Id) if dbErr != nil { diff --git a/server/internal/model/input/sysin/im_tenant.go b/server/internal/model/input/sysin/im_tenant.go index b94d20c..8fbeba4 100644 --- a/server/internal/model/input/sysin/im_tenant.go +++ b/server/internal/model/input/sysin/im_tenant.go @@ -18,17 +18,6 @@ import ( "github.com/gogf/gf/v2/os/gtime" ) -// ImTenantUpdateFields 修改IM租户字段过滤 -type ImTenantUpdateFields struct { - Code string `json:"code" dc:"租户编码"` - Name string `json:"name" dc:"租户名称"` - TenantDesc string `json:"tenantDesc" dc:"租户描述"` - DbName string `json:"dbName" dc:"业务库名"` - DbDsn string `json:"dbDsn" dc:"独立连接串"` - State int `json:"state" dc:"状态"` - UpdatedBy int64 `json:"updatedBy" dc:"更新者"` -} - // ImTenantInsertFields 新增IM租户字段过滤 type ImTenantInsertFields struct { Code string `json:"code" dc:"租户编码"` @@ -76,6 +65,8 @@ type ImTenantEditInp struct { LocalModerationEnabled int `json:"localModerationEnabled" dc:"是否启用本地内容审核"` MessageEncryptionEnabled int `json:"messageEncryptionEnabled" dc:"是否启用消息加密"` RealnameReviewPolicy int `json:"realnameReviewPolicy" dc:"实名审核策略"` + AdminUsername string `json:"adminUsername" dc:"新增商户时自动创建的后台管理员账号名"` + AdminPassword string `json:"adminPassword" dc:"新增商户时自动创建的后台管理员初始密码"` } func (in *ImTenantEditInp) Filter(ctx context.Context) (err error) { @@ -85,9 +76,6 @@ func (in *ImTenantEditInp) Filter(ctx context.Context) (err error) { if e := g.Validator().Rules("required|max-length:128").Data(in.Name).Messages("租户名称不能为空|租户名称不能超过128个字符").Run(ctx); e != nil { return e.Current() } - if in.DbName == "" && in.DbDsn == "" { - return gerror.New("业务库名和独立连接串至少填写一个") - } if !dict.HasOptionKey(consts.ImTenantStateOptions, in.State) { return gerror.New("租户状态不正确") } @@ -99,10 +87,20 @@ func (in *ImTenantEditInp) Filter(ctx context.Context) (err error) { if in.RealnameReviewPolicy != 0 && in.RealnameReviewPolicy != 1 { return gerror.New("实名审核策略不正确") } + if in.Id == 0 { + if e := g.Validator().Rules("required|length:3,32|regex:^[a-zA-Z0-9_-]+$").Data(in.AdminUsername).Messages("管理员账号不能为空|管理员账号长度应为3到32位|管理员账号仅支持字母、数字、下划线和横线").Run(ctx); e != nil { + return e.Current() + } + if e := g.Validator().Rules("required|length:6,32").Data(in.AdminPassword).Messages("管理员密码不能为空|管理员密码长度应为6到32位").Run(ctx); e != nil { + return e.Current() + } + } return } -type ImTenantEditModel struct{} +type ImTenantEditModel struct { + AdminUsername string `json:"adminUsername" dc:"已创建的后台管理员账号名"` +} // ImTenantViewInp 获取指定IM租户信息 type ImTenantViewInp struct { diff --git a/web/src/views/imTenant/edit.vue b/web/src/views/imTenant/edit.vue index 5c54a19..373fa15 100644 --- a/web/src/views/imTenant/edit.vue +++ b/web/src/views/imTenant/edit.vue @@ -37,16 +37,17 @@ - + - + + + + + + + + + + + - - - - -