From 0596599db9506627f970f1aeb31b896add53711d Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 22 Sep 2026 11:34:40 +0800 Subject: [PATCH] feat: add tenant bound restriction for IM dashboard and management pages 1. extract isTenantBound utility from user store logic 2. add disabled state to tenant select components across all IM pages 3. update policy tenant options logic to hide global option for bound tenants 4. add backend scope validation for IM admin operations --- server/internal/logic/sys/im_admin_scope.go | 38 ++++++++++++++ server/internal/logic/sys/im_dead_letter.go | 27 ++-------- server/internal/logic/sys/im_kafka_command.go | 37 +++++++++++--- web/src/views/im/registrationInvite/model.ts | 3 +- web/src/views/imAbuseReason/model.ts | 3 +- web/src/views/imAuditLog/model.ts | 3 +- web/src/views/imDashboard/index.vue | 2 + web/src/views/imDeadLetter/model.ts | 3 +- web/src/views/imExternalConfig/model.ts | 3 +- web/src/views/imFeedbackConfig/index.vue | 2 + web/src/views/imFile/model.ts | 3 +- web/src/views/imFilePolicy/model.ts | 3 +- web/src/views/imGroup/model.ts | 3 +- web/src/views/imKafkaCommand/model.ts | 3 +- web/src/views/imLoginFailureLog/model.ts | 2 + web/src/views/imMailboxConfig/index.vue | 2 + web/src/views/imMessageShared/model.ts | 2 + web/src/views/imNotification/model.ts | 3 +- web/src/views/imOrg/model.ts | 3 +- web/src/views/imOutbox/model.ts | 3 +- web/src/views/imPolicyShared.ts | 6 +-- web/src/views/imRealname/model.ts | 3 +- web/src/views/imSecurityPolicy/model.ts | 3 +- web/src/views/imSensitiveWord/model.ts | 3 +- web/src/views/imTaskDashboard/model.ts | 3 +- web/src/views/imTenantOptions.ts | 51 +++++++++++-------- web/src/views/imUser/model.ts | 3 +- web/src/views/imUserLoginHistory/model.ts | 2 + web/src/views/imUserPolicyGroup/model.ts | 3 +- web/src/views/imWorkOrder/model.ts | 3 +- web/src/views/imWorkbench/model.ts | 3 +- 31 files changed, 154 insertions(+), 77 deletions(-) diff --git a/server/internal/logic/sys/im_admin_scope.go b/server/internal/logic/sys/im_admin_scope.go index 054e367..714c6ef 100644 --- a/server/internal/logic/sys/im_admin_scope.go +++ b/server/internal/logic/sys/im_admin_scope.go @@ -17,6 +17,7 @@ import ( "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gtime" + "strings" ) type imAdminScope struct { @@ -85,6 +86,43 @@ func resolveImAdminBoundTenant(ctx context.Context) (int64, error) { return tenantId, nil } +// ensureImRecordTenantAllowed 校验目标记录/租户是否在当前操作者允许范围内。 +// 平台管理员(超管/平台角色/未绑定租户的公司部门)可访问任意租户的记录; +// 其余账号强制限定为自己绑定的租户(与 resolveImAdminScope 的范围逻辑一致)。 +// 适用于入参不含租户ID、需按已查出的记录租户做后置校验的场景。 +func ensureImRecordTenantAllowed(ctx context.Context, recordTenantId int64) error { + if isImPlatformAdmin(ctx, contexts.GetUserId(ctx)) { + return nil + } + if !contexts.IsTenantDept(ctx) && !(contexts.IsCompanyDept(ctx) && contexts.GetTenantId(ctx) > 0) { + return gerror.New("当前账号无权管理IM租户") + } + boundTenantId, err := resolveImAdminBoundTenant(ctx) + if err != nil { + return err + } + if boundTenantId != recordTenantId { + return gerror.New("无权访问其他商户的IM数据") + } + return nil +} + +// lookupImTenantByCode 按租户编码查询租户(用于入参只有租户编码的管理端接口) +func lookupImTenantByCode(ctx context.Context, code string) (tenant *entity.ImTenant, err error) { + code = strings.TrimSpace(code) + if code == "" { + return nil, gerror.New("请选择IM租户") + } + if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx). + Where(dao.ImTenant.Columns().Code, code).Scan(&tenant); err != nil { + return nil, gerror.Wrap(err, "获取IM租户失败") + } + if tenant == nil || tenant.Id <= 0 { + return nil, gerror.New("IM租户不存在") + } + return tenant, nil +} + func ensureImTenantActive(ctx context.Context, tenantId int64) (err error) { var tenant *entity.ImTenant if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil { diff --git a/server/internal/logic/sys/im_dead_letter.go b/server/internal/logic/sys/im_dead_letter.go index 11d16f9..86b934c 100644 --- a/server/internal/logic/sys/im_dead_letter.go +++ b/server/internal/logic/sys/im_dead_letter.go @@ -10,18 +10,13 @@ import ( "encoding/json" "fmt" "hotgo/internal/consts" - "hotgo/internal/dao" "hotgo/internal/library/contexts" "hotgo/internal/library/imadmin" "hotgo/internal/library/imkafka" - "hotgo/internal/library/imtenant" - "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" "net/url" "strconv" - - "github.com/gogf/gf/v2/errors/gerror" ) type sSysImDeadLetter struct{} @@ -39,7 +34,7 @@ func (s *sSysImDeadLetter) List(ctx context.Context, in *sysin.ImDeadLetterListI if err = in.Filter(ctx); err != nil { return nil, err } - tenant, err := s.getTenant(ctx, in.TenantId) + tenant, err := resolveImAdminTenant(ctx, in.TenantId) if err != nil { return nil, err } @@ -81,7 +76,7 @@ func (s *sSysImDeadLetter) View(ctx context.Context, in *sysin.ImDeadLetterViewI if err = in.Filter(ctx); err != nil { return nil, err } - tenant, err := s.getTenant(ctx, in.TenantId) + tenant, err := resolveImAdminTenant(ctx, in.TenantId) if err != nil { return nil, err } @@ -120,7 +115,7 @@ func (s *sSysImDeadLetter) publishActionCommand(ctx context.Context, in *sysin.I if err := in.Filter(ctx); err != nil { return nil, err } - tenant, err := s.getTenant(ctx, in.TenantId) + tenant, err := resolveImAdminTenant(ctx, in.TenantId) if err != nil { return nil, err } @@ -162,22 +157,6 @@ func (s *sSysImDeadLetter) publishActionCommand(ctx context.Context, in *sysin.I }, nil } -func (s *sSysImDeadLetter) getTenant(ctx context.Context, tenantId int64) (tenant *entity.ImTenant, err error) { - if tenantId <= 0 { - return nil, gerror.New("请选择IM租户") - } - if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil { - return nil, gerror.Wrap(err, "获取IM租户失败") - } - if tenant == nil || tenant.Id <= 0 { - return nil, gerror.New("IM租户不存在") - } - if tenant.State != consts.ImTenantStateActive { - return nil, gerror.New("IM租户未启用") - } - return tenant, nil -} - func convertDeadLetterModel(in *sysin.ImDeadLetterListModelRaw) *sysin.ImDeadLetterListModel { if in == nil { return &sysin.ImDeadLetterListModel{} diff --git a/server/internal/logic/sys/im_kafka_command.go b/server/internal/logic/sys/im_kafka_command.go index ae97609..d1e4717 100644 --- a/server/internal/logic/sys/im_kafka_command.go +++ b/server/internal/logic/sys/im_kafka_command.go @@ -33,7 +33,12 @@ func (s *sSysImKafkaCommand) List(ctx context.Context, in *sysin.ImKafkaCommandL if err = in.Filter(ctx); err != nil { return } - where, args := buildImKafkaCommandWhere(in) + // 强制租户范围:商户绑定账号忽略前端所选租户,平台管理员使用所选租户 + scope, err := resolveImAdminScope(ctx, in.TenantId) + if err != nil { + return + } + where, args := buildImKafkaCommandWhere(in, scope.TenantId) db := imtenant.RegistryDB() totalValue, err := db.GetValue(ctx, "SELECT COUNT(*) FROM im_admin_commands "+where, args...) if err != nil { @@ -91,16 +96,16 @@ func (s *sSysImKafkaCommand) View(ctx context.Context, in *sysin.ImKafkaCommandV if err = row.Struct(&res); err != nil { return nil, gerror.Wrap(err, "解析IM后台命令详情失败,请稍后重试!") } + // 后置校验记录归属租户是否在操作者允许范围内 + if err = ensureImRecordTenantAllowed(ctx, res.TenantId); err != nil { + return nil, err + } return } -func buildImKafkaCommandWhere(in *sysin.ImKafkaCommandListInp) (string, []any) { - parts := []string{"1=1"} - args := []any{} - if in.TenantId > 0 { - parts = append(parts, "tenant_id=?") - args = append(args, in.TenantId) - } +func buildImKafkaCommandWhere(in *sysin.ImKafkaCommandListInp, scopeTenantId int64) (string, []any) { + parts := []string{"tenant_id=?"} + args := []any{scopeTenantId} if in.CommandId != "" { parts = append(parts, "command_id LIKE ?") args = append(args, "%"+in.CommandId+"%") @@ -136,6 +141,14 @@ func (s *sSysImKafkaCommand) CacheInvalidate(ctx context.Context, in *sysin.ImKa if err = in.Filter(ctx); err != nil { return nil, err } + // 校验目标租户是否在操作者允许范围内 + tenant, err := lookupImTenantByCode(ctx, in.TenantCode) + if err != nil { + return nil, err + } + if err = ensureImRecordTenantAllowed(ctx, tenant.Id); err != nil { + return nil, err + } user := contexts.GetUser(ctx) actor := &imkafka.Actor{ Type: "admin", @@ -170,6 +183,14 @@ func (s *sSysImKafkaCommand) WorkbenchEvent(ctx context.Context, in *sysin.ImKaf if err = in.Filter(ctx); err != nil { return nil, err } + // 校验目标租户是否在操作者允许范围内 + tenant, err := lookupImTenantByCode(ctx, in.TenantCode) + if err != nil { + return nil, err + } + if err = ensureImRecordTenantAllowed(ctx, tenant.Id); err != nil { + return nil, err + } user := contexts.GetUser(ctx) actor := &imkafka.Actor{ Type: "admin", diff --git a/web/src/views/im/registrationInvite/model.ts b/web/src/views/im/registrationInvite/model.ts index d6eaf32..05fd151 100644 --- a/web/src/views/im/registrationInvite/model.ts +++ b/web/src/views/im/registrationInvite/model.ts @@ -2,7 +2,7 @@ import { computed, ref } from 'vue'; import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -40,6 +40,7 @@ export const schemas = ref([ componentProps: { clearable: true, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imAbuseReason/model.ts b/web/src/views/imAbuseReason/model.ts index 658bf32..dfd7a63 100644 --- a/web/src/views/imAbuseReason/model.ts +++ b/web/src/views/imAbuseReason/model.ts @@ -2,7 +2,7 @@ import { computed, ref } from 'vue'; import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -42,6 +42,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imAuditLog/model.ts b/web/src/views/imAuditLog/model.ts index ae79bca..4dae81e 100644 --- a/web/src/views/imAuditLog/model.ts +++ b/web/src/views/imAuditLog/model.ts @@ -2,7 +2,7 @@ import { computed, ref } from 'vue'; import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; export const tenantOptions = ref([]); @@ -45,6 +45,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imDashboard/index.vue b/web/src/views/imDashboard/index.vue index d3d1ba3..aed1570 100644 --- a/web/src/views/imDashboard/index.vue +++ b/web/src/views/imDashboard/index.vue @@ -7,6 +7,7 @@ @@ -40,6 +41,7 @@ import { useMessage } from 'naive-ui'; import { Overview, Trends } from '@/api/imDashboard'; import { imQueryTenantOptions, loadImQueryTenantOptions } from '@/views/imAdminQueryShared'; + import { isTenantBound } from '@/views/imTenantOptions'; const message = useMessage(); const tenantId = ref(0); diff --git a/web/src/views/imDeadLetter/model.ts b/web/src/views/imDeadLetter/model.ts index a2eddc3..e1d1f02 100644 --- a/web/src/views/imDeadLetter/model.ts +++ b/web/src/views/imDeadLetter/model.ts @@ -2,7 +2,7 @@ import { computed, ref } from 'vue'; import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -50,6 +50,7 @@ export const schemas = ref([ componentProps: { clearable: true, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imExternalConfig/model.ts b/web/src/views/imExternalConfig/model.ts index 41f2e87..0f3f5c6 100644 --- a/web/src/views/imExternalConfig/model.ts +++ b/web/src/views/imExternalConfig/model.ts @@ -2,7 +2,7 @@ import { computed, ref } from 'vue'; import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -46,6 +46,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imFeedbackConfig/index.vue b/web/src/views/imFeedbackConfig/index.vue index 5d98ba4..18727c2 100644 --- a/web/src/views/imFeedbackConfig/index.vue +++ b/web/src/views/imFeedbackConfig/index.vue @@ -15,6 +15,7 @@ @@ -72,6 +73,7 @@ import { useMessage } from 'naive-ui'; import { usePermission } from '@/hooks/web/usePermission'; import { Get, Update } from '@/api/imFeedbackConfig'; + import { isTenantBound } from '@/views/imTenantOptions'; import { loadOptions, loadTenantOptions, newState, rules, State, tenantOptions } from './model'; const message = useMessage(); diff --git a/web/src/views/imFile/model.ts b/web/src/views/imFile/model.ts index 439084d..16e3a3b 100644 --- a/web/src/views/imFile/model.ts +++ b/web/src/views/imFile/model.ts @@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -49,6 +49,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imFilePolicy/model.ts b/web/src/views/imFilePolicy/model.ts index efa914a..3a56e88 100644 --- a/web/src/views/imFilePolicy/model.ts +++ b/web/src/views/imFilePolicy/model.ts @@ -2,7 +2,7 @@ import { computed, ref } from 'vue'; import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -42,6 +42,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imGroup/model.ts b/web/src/views/imGroup/model.ts index 1931326..fffeee9 100644 --- a/web/src/views/imGroup/model.ts +++ b/web/src/views/imGroup/model.ts @@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -122,6 +122,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imKafkaCommand/model.ts b/web/src/views/imKafkaCommand/model.ts index 32244b6..ea3aca8 100644 --- a/web/src/views/imKafkaCommand/model.ts +++ b/web/src/views/imKafkaCommand/model.ts @@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -52,6 +52,7 @@ export const schemas = ref([ componentProps: { clearable: true, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imLoginFailureLog/model.ts b/web/src/views/imLoginFailureLog/model.ts index edb4efc..d3ca331 100644 --- a/web/src/views/imLoginFailureLog/model.ts +++ b/web/src/views/imLoginFailureLog/model.ts @@ -3,6 +3,7 @@ import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; import { useDictStore } from '@/store/modules/dict'; import { imQueryTenantOptions } from '@/views/imAdminQueryShared'; +import { isTenantBound } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -13,6 +14,7 @@ export const schemas = ref([ label: '商户', componentProps: { clearable: false, + disabled: isTenantBound(), options: computed(() => imQueryTenantOptions.value), placeholder: '请选择商户', }, diff --git a/web/src/views/imMailboxConfig/index.vue b/web/src/views/imMailboxConfig/index.vue index 8b8ba5a..2a403bf 100644 --- a/web/src/views/imMailboxConfig/index.vue +++ b/web/src/views/imMailboxConfig/index.vue @@ -15,6 +15,7 @@ @@ -78,6 +79,7 @@ import { useMessage } from 'naive-ui'; import { usePermission } from '@/hooks/web/usePermission'; import { Get, Update } from '@/api/imMailboxConfig'; + import { isTenantBound } from '@/views/imTenantOptions'; import { loadOptions, loadTenantOptions, newState, rules, State, tenantOptions } from './model'; const message = useMessage(); diff --git a/web/src/views/imMessageShared/model.ts b/web/src/views/imMessageShared/model.ts index 0663d6e..0a7de41 100644 --- a/web/src/views/imMessageShared/model.ts +++ b/web/src/views/imMessageShared/model.ts @@ -3,6 +3,7 @@ import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; import { useDictStore } from '@/store/modules/dict'; import { imQueryTenantOptions } from '@/views/imAdminQueryShared'; +import { isTenantBound } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -14,6 +15,7 @@ export function createMessageSchemas(group: boolean) { label: '商户', componentProps: { clearable: false, + disabled: isTenantBound(), options: computed(() => imQueryTenantOptions.value), placeholder: '请选择商户', }, diff --git a/web/src/views/imNotification/model.ts b/web/src/views/imNotification/model.ts index 0afe0d1..31d5615 100644 --- a/web/src/views/imNotification/model.ts +++ b/web/src/views/imNotification/model.ts @@ -2,7 +2,7 @@ import { computed, ref } from 'vue'; import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -44,6 +44,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imOrg/model.ts b/web/src/views/imOrg/model.ts index 4e2aa2f..9a6d3d6 100644 --- a/web/src/views/imOrg/model.ts +++ b/web/src/views/imOrg/model.ts @@ -2,7 +2,7 @@ import { computed, ref } from 'vue'; import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -77,6 +77,7 @@ export const departmentSchemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imOutbox/model.ts b/web/src/views/imOutbox/model.ts index 2e252e4..0321961 100644 --- a/web/src/views/imOutbox/model.ts +++ b/web/src/views/imOutbox/model.ts @@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -48,6 +48,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imPolicyShared.ts b/web/src/views/imPolicyShared.ts index a015681..b13ea74 100644 --- a/web/src/views/imPolicyShared.ts +++ b/web/src/views/imPolicyShared.ts @@ -1,6 +1,5 @@ import { ref } from 'vue'; -import { useUserStore } from '@/store/modules/user'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; export { getDefaultTenantValue } from '@/views/imTenantOptions'; @@ -9,7 +8,8 @@ const tenantOnlyOptions = ref([]); export async function loadPolicyTenantOptions(includeGlobal = true) { const tenants = await loadImTenantOptions(tenantOnlyOptions, { page: 1, perPage: 100 }); - const canUseGlobal = includeGlobal && useUserStore().isCompanyDept; + // 商户绑定账号不显示“全局默认”,仅平台管理员(未绑定租户)可配置全局策略 + const canUseGlobal = includeGlobal && !isTenantBound(); policyTenantOptions.value = canUseGlobal ? [{ label: '全局默认', value: 0 }, ...tenants] : tenants; diff --git a/web/src/views/imRealname/model.ts b/web/src/views/imRealname/model.ts index d89d3fd..e2da202 100644 --- a/web/src/views/imRealname/model.ts +++ b/web/src/views/imRealname/model.ts @@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -46,6 +46,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imSecurityPolicy/model.ts b/web/src/views/imSecurityPolicy/model.ts index 01c8f26..2dc51c2 100644 --- a/web/src/views/imSecurityPolicy/model.ts +++ b/web/src/views/imSecurityPolicy/model.ts @@ -2,6 +2,7 @@ import { computed, ref } from 'vue'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; import { policyTenantOptions } from '@/views/imPolicyShared'; +import { isTenantBound } from '@/views/imTenantOptions'; const dict = useDictStore(); export const schemas = ref([ @@ -9,7 +10,7 @@ export const schemas = ref([ field: 'tenantId', component: 'NSelect', label: '商户', - componentProps: { options: computed(() => policyTenantOptions.value), clearable: false }, + componentProps: { options: computed(() => policyTenantOptions.value), clearable: false, disabled: isTenantBound() }, }, { field: 'publishStatus', diff --git a/web/src/views/imSensitiveWord/model.ts b/web/src/views/imSensitiveWord/model.ts index 8067076..7c6a5cf 100644 --- a/web/src/views/imSensitiveWord/model.ts +++ b/web/src/views/imSensitiveWord/model.ts @@ -3,6 +3,7 @@ import { NTag } from 'naive-ui'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; import { policyTenantOptions } from '@/views/imPolicyShared'; +import { isTenantBound } from '@/views/imTenantOptions'; import { renderOptionTag } from '@/utils'; const dict = useDictStore(); @@ -12,7 +13,7 @@ export const schemas = ref([ field: 'tenantId', component: 'NSelect', label: '商户', - componentProps: { options: computed(() => policyTenantOptions.value), clearable: false }, + componentProps: { options: computed(() => policyTenantOptions.value), clearable: false, disabled: isTenantBound() }, }, { field: 'word', component: 'NInput', label: '敏感词', componentProps: { clearable: true } }, { diff --git a/web/src/views/imTaskDashboard/model.ts b/web/src/views/imTaskDashboard/model.ts index e1493bd..3ad10fc 100644 --- a/web/src/views/imTaskDashboard/model.ts +++ b/web/src/views/imTaskDashboard/model.ts @@ -1,7 +1,7 @@ import { computed, ref } from 'vue'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -34,6 +34,7 @@ export const schemas = ref([ componentProps: { clearable: true, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imTenantOptions.ts b/web/src/views/imTenantOptions.ts index 2f1bb48..4237f28 100644 --- a/web/src/views/imTenantOptions.ts +++ b/web/src/views/imTenantOptions.ts @@ -1,6 +1,7 @@ import { Ref } from 'vue'; import { List as TenantList } from '@/api/imTenant'; import { useUserStore } from '@/store/modules/user'; +import type { FormSchema } from '@/components/Form'; export interface ImTenantOption { label: string; @@ -26,15 +27,10 @@ export async function loadImTenantOptions( target: Ref, params: Record = { page: 1, perPage: 100 } ) { - const userStore = useUserStore(); - const userInfo = { ...readTokenUserInfo(userStore.getToken), ...(userStore.getUserInfo as any) }; - const boundTenantId = Number(userInfo?.tenantId || userInfo?.tenantID || 0); + const boundTenantId = getBoundTenantId(); - if (!userStore.isCompanyDept) { - if (boundTenantId <= 0) { - target.value = []; - return target.value; - } + // 登录用户已绑定商户租户:仅返回自己商户选项(与后端 resolveImAdminScope 强制范围一致) + if (boundTenantId > 0) { let tenant: any = null; try { const res = await TenantList({ ...params, id: boundTenantId, page: 1, perPage: 1 }); @@ -42,20 +38,11 @@ export async function loadImTenantOptions( } catch { tenant = null; } - target.value = [ - tenant - ? toTenantOption(tenant) - : { - label: formatImTenantLabel({ - id: boundTenantId, - name: userInfo?.tenantName, - code: userInfo?.tenantCode, - }), - value: boundTenantId, - code: userInfo?.tenantCode, - name: userInfo?.tenantName, - }, - ]; + if (!tenant) { + const info = readTokenUserInfo(useUserStore().getToken); + tenant = { id: boundTenantId, name: info?.tenantName, code: info?.tenantCode }; + } + target.value = [toTenantOption(tenant)]; return target.value; } @@ -64,6 +51,26 @@ export async function loadImTenantOptions( return target.value; } +// 登录用户绑定的商户租户ID(按绑定租户判定,与后端 resolveImAdminScope 的范围逻辑一致) +export function getBoundTenantId(): number { + const userStore = useUserStore(); + const userInfo = { ...readTokenUserInfo(userStore.getToken), ...(userStore.getUserInfo as any) }; + return Number(userInfo?.tenantId || userInfo?.tenantID || 0); +} + +// 登录用户是否为商户绑定账号(true=下拉框只显示并锁定自己商户) +export function isTenantBound(): boolean { + return getBoundTenantId() > 0; +} + +// 商户绑定账号:锁定搜索表单中的商户下拉项(仅一项、不可清除、不可更改) +export function lockTenantSchema(schema?: FormSchema) { + if (!schema || !isTenantBound()) return; + const props: any = (schema.componentProps = schema.componentProps || {}); + props.disabled = true; + props.clearable = false; +} + function toTenantOption(item: any): ImTenantOption { return { label: formatImTenantLabel(item), diff --git a/web/src/views/imUser/model.ts b/web/src/views/imUser/model.ts index c5e739b..14d0173 100644 --- a/web/src/views/imUser/model.ts +++ b/web/src/views/imUser/model.ts @@ -3,7 +3,7 @@ import { NImage, NTag } from 'naive-ui'; import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; import { List as PolicyGroupList } from '@/api/imUserPolicyGroup'; import defaultAvatar from '@/assets/images/schoolboy.png'; @@ -86,6 +86,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imUserLoginHistory/model.ts b/web/src/views/imUserLoginHistory/model.ts index 82f8319..9319330 100644 --- a/web/src/views/imUserLoginHistory/model.ts +++ b/web/src/views/imUserLoginHistory/model.ts @@ -3,6 +3,7 @@ import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; import { useDictStore } from '@/store/modules/dict'; import { imQueryTenantOptions } from '@/views/imAdminQueryShared'; +import { isTenantBound } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -13,6 +14,7 @@ export const schemas = ref([ label: '商户', componentProps: { clearable: false, + disabled: isTenantBound(), options: computed(() => imQueryTenantOptions.value), placeholder: '请选择商户', }, diff --git a/web/src/views/imUserPolicyGroup/model.ts b/web/src/views/imUserPolicyGroup/model.ts index d2e901a..48c1179 100644 --- a/web/src/views/imUserPolicyGroup/model.ts +++ b/web/src/views/imUserPolicyGroup/model.ts @@ -2,6 +2,7 @@ import { computed, ref } from 'vue'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; import { policyTenantOptions } from '@/views/imPolicyShared'; +import { isTenantBound } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -24,7 +25,7 @@ export const schemas = ref([ field: 'tenantId', component: 'NSelect', label: '商户', - componentProps: { options: computed(() => policyTenantOptions.value), clearable: false }, + componentProps: { options: computed(() => policyTenantOptions.value), clearable: false, disabled: isTenantBound() }, }, { field: 'keyword', diff --git a/web/src/views/imWorkOrder/model.ts b/web/src/views/imWorkOrder/model.ts index 7e0c16d..adaf26c 100644 --- a/web/src/views/imWorkOrder/model.ts +++ b/web/src/views/imWorkOrder/model.ts @@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { defRangeShortcuts } from '@/utils/dateUtil'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -88,6 +88,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, }, diff --git a/web/src/views/imWorkbench/model.ts b/web/src/views/imWorkbench/model.ts index f107121..1e24d2b 100644 --- a/web/src/views/imWorkbench/model.ts +++ b/web/src/views/imWorkbench/model.ts @@ -2,7 +2,7 @@ import { computed, ref } from 'vue'; import { cloneDeep } from 'lodash-es'; import { FormSchema } from '@/components/Form'; import { useDictStore } from '@/store/modules/dict'; -import { loadImTenantOptions } from '@/views/imTenantOptions'; +import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions'; const dict = useDictStore(); @@ -44,6 +44,7 @@ export const schemas = ref([ componentProps: { clearable: false, options: computed(() => tenantOptions.value), + disabled: isTenantBound(), placeholder: '请选择商户', }, },