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: '请选择商户', }, },