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 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-