feat: 完善多租户客户切换和数据隔离功能

## 主要改进

### 1. 修复JavaScript数字精度问题
- 将所有customerId从number类型改为string类型,避免大整数精度丢失
- 修复customerId=1968304677554302978被截断为1968304677554303000的问题
- 更新CustomerSwitcher、CustomerSelector等组件的类型定义

### 2. 完善客户切换功能
- 新增TenantInfo组件显示当前租户信息
- 修复CustomerSwitcher下拉选择事件处理
- 完善客户切换的用户体验和错误处理

### 3. 实现真正的多租户数据隔离
- 后端SysOrgUnitsSearchDTO新增customerId字段支持
- SysOrgUnitsFacadeImpl中实现customerId到id的自动转换
- 修改/sys_org_units/page和/sys_org_units/tree接口支持customerId过滤

### 4. 优化API调用机制
- 前端移除手动传递id=0的逻辑
- 让请求拦截器自动注入customerId参数
- 后端自动将customerId转换为有效的查询条件

### 5. 新增客户配置管理
- TCustomerConfigController新增/all-customers接口
- 为客户选择器提供标准的客户列表API

## 技术细节
- 前端:Vue 3 + TypeScript + Pinia状态管理
- 后端:Spring Boot + MyBatis-Plus
- 数据隔离:customerId即sys_org_units表的id字段
- 精度安全:JavaScript大整数使用string类型处理

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
BrunoGao
2025-09-17 22:41:37 +08:00
co-authored by Claude
parent 57a55b7c10
commit fb4c23da4e
26 changed files with 673 additions and 65 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ LJWX_ADMIN_VERSION=2.0.1
# 服务器IP地址 - 根据实际部署环境修改
SERVER_IP=192.168.1.83
# 各服务外部端口配置
MYSQL_EXTERNAL_PORT=3306
REDIS_EXTERNAL_PORT=6379
MYSQL_EXTERNAL_PORT=23306
REDIS_EXTERNAL_PORT=26379
LJWX_BOOT_EXTERNAL_PORT=9999
LJWX_BIGSCREEN_EXTERNAL_PORT=8001
LJWX_ADMIN_EXTERNAL_PORT=8088
@@ -0,0 +1,121 @@
<template>
<div class="customer-selector">
<NSelect
v-model:value="selectedCustomerId"
:options="customerOptions"
:loading="loading"
placeholder="请选择客户"
filterable
clearable
@update:value="handleCustomerChange"
>
<template #empty>
<div class="text-center py-4 text-gray-500">
{{ loading ? '加载中...' : '暂无客户数据' }}
</div>
</template>
</NSelect>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import type { SelectOption } from 'naive-ui';
import { NSelect } from 'naive-ui';
import { fetchGetAllAvailableCustomers } from '@/service/api/customer/customer';
import { useAuthStore } from '@/store/modules/auth';
interface CustomerOption {
customerId: string;
customerName: string;
}
const emit = defineEmits<{
change: [customerId: string | null, customerName: string | null];
}>();
const authStore = useAuthStore();
const loading = ref(false);
const customers = ref<CustomerOption[]>([]);
const selectedCustomerId = ref<string | null>(null);
// 转换为 NSelect 需要的选项格式
const customerOptions = computed<SelectOption[]>(() => {
return customers.value.map(customer => ({
label: customer.customerName,
value: customer.customerId
}));
});
// 获取客户列表
const loadCustomers = async () => {
try {
loading.value = true;
const { data } = await fetchGetAllAvailableCustomers();
// 转换 customerId 为字符串类型避免精度问题
customers.value = (data || []).map(item => ({
customerId: item.customerId.toString(),
customerName: item.customerName
}));
// 如果当前用户有 customerId,设置为默认选中
if (authStore.userInfo.customerId && authStore.userInfo.customerId !== 0) {
selectedCustomerId.value = authStore.userInfo.customerId.toString();
}
} catch (error) {
console.error('加载客户列表失败:', error);
customers.value = [];
} finally {
loading.value = false;
}
};
// 处理客户切换
const handleCustomerChange = (customerId: string | null) => {
const selectedCustomer = customers.value.find(c => c.customerId === customerId);
const customerName = selectedCustomer?.customerName || null;
console.log('客户切换:', { customerId, customerName });
// 发送事件给父组件
emit('change', customerId, customerName);
// 这里可以设置全局的 customerId
// 例如保存到 localStorage 或者 store 中
if (customerId) {
localStorage.setItem('selectedCustomerId', customerId);
localStorage.setItem('selectedCustomerName', customerName || '');
} else {
localStorage.removeItem('selectedCustomerId');
localStorage.removeItem('selectedCustomerName');
}
};
// 获取当前选中的客户信息
const getCurrentCustomer = () => {
return customers.value.find(c => c.customerId === selectedCustomerId.value) || null;
};
// 设置选中的客户
const setSelectedCustomer = (customerId: string | null) => {
selectedCustomerId.value = customerId;
handleCustomerChange(customerId);
};
// 暴露方法给父组件
defineExpose({
getCurrentCustomer,
setSelectedCustomer,
loadCustomers
});
onMounted(() => {
loadCustomers();
});
</script>
<style scoped>
.customer-selector {
min-width: 200px;
}
</style>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, h } from 'vue';
import { NDropdown, NButton, NInput, NSpace, NSpin, NEmpty, NTag, NAvatar } from 'naive-ui';
import type { DropdownOption } from 'naive-ui';
import { useCustomerStore } from '@/store/modules/customer';
@@ -66,10 +66,7 @@ const dropdownOptions = computed((): DropdownOption[] => {
.slice(0, 3)
.map(customer => ({
key: `recent-${customer.id}`,
label: customer.name,
props: {
onClick: () => handleCustomerSwitch(customer.id)
}
label: customer.name
}))
});
}
@@ -84,10 +81,7 @@ const dropdownOptions = computed((): DropdownOption[] => {
.filter(customer => customer.id !== currentCustomer.value?.id)
.map(customer => ({
key: `all-${customer.id}`,
label: customer.name,
props: {
onClick: () => handleCustomerSwitch(customer.id)
}
label: customer.name
}))
});
}
@@ -95,15 +89,29 @@ const dropdownOptions = computed((): DropdownOption[] => {
return options;
});
// 处理下拉选择
const handleSelect = (key: string) => {
console.log('下拉选择:', key);
// 解析客户ID
const match = key.match(/(recent|all)-(.+)/);
if (match) {
const customerId = match[2]; // 保持为字符串
handleCustomerSwitch(customerId);
}
};
// 处理客户切换
const handleCustomerSwitch = async (customerId: number) => {
const handleCustomerSwitch = async (customerId: string) => { // 改为字符串类型
try {
console.log('开始切换客户:', customerId);
await customerStore.switchCustomer(customerId);
dropdownVisible.value = false;
searchValue.value = '';
console.log('客户切换成功');
} catch (error) {
console.error('切换客户失败:', error);
// 这里可以添加错误提示
window.$message?.error('切换客户失败');
}
};
@@ -130,8 +138,17 @@ const getCustomerAvatar = (customer: typeof currentCustomer.value) => {
};
onMounted(() => {
console.log('[CustomerSwitcher] 组件挂载:', {
canSwitch: canSwitch.value,
currentCustomer: currentCustomer.value,
currentCustomerId: customerStore.currentCustomerId,
customerListLength: customerStore.customerList.length,
userName: authStore.userInfo?.userName
});
// 组件挂载时加载客户列表(如果有权限)
if (canSwitch.value && customerStore.customerList.length === 0) {
console.log('[CustomerSwitcher] 开始加载客户列表');
customerStore.loadCustomerList();
}
});
@@ -148,6 +165,7 @@ onMounted(() => {
size="medium"
:show-arrow="false"
@show="handleDropdownShow"
@select="handleSelect"
>
<template #default>
<NButton
@@ -0,0 +1,83 @@
<template>
<div v-if="showTenantInfo" class="tenant-info">
<div class="tenant-badge">
<SvgIcon icon="mdi:domain" class="text-blue-500" />
<span class="tenant-text">
<span class="label">当前租户:</span>
<span class="tenant-name">{{ currentCustomer?.name || '未选择' }}</span>
<span v-if="currentCustomer?.id" class="tenant-id">(ID: {{ currentCustomer.id }})</span>
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, watchEffect } from 'vue';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { useAuthStore } from '@/store/modules/auth';
import { useCustomerStore } from '@/store/modules/customer';
defineOptions({
name: 'TenantInfo'
});
const authStore = useAuthStore();
const customerStore = useCustomerStore();
// 当前客户信息
const currentCustomer = computed(() => customerStore.currentCustomer);
// 是否显示租户信息(只有admin用户才显示)
const showTenantInfo = computed(() => {
return authStore.userInfo?.userName === 'admin' && customerStore.canSwitchCustomer;
});
// 调试信息
watchEffect(() => {
console.log('[TenantInfo] 状态更新:', {
showTenantInfo: showTenantInfo.value,
currentCustomer: currentCustomer.value,
currentCustomerId: customerStore.currentCustomerId,
userName: authStore.userInfo?.userName,
canSwitch: customerStore.canSwitchCustomer
});
});
</script>
<style scoped>
.tenant-info {
@apply flex items-center;
}
.tenant-badge {
@apply flex items-center px-3 py-1.5 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-700;
@apply text-sm font-medium;
}
.tenant-text {
@apply ml-2 flex items-center space-x-1;
}
.label {
@apply text-gray-600 dark:text-gray-400;
}
.tenant-name {
@apply text-blue-700 dark:text-blue-300 font-semibold;
}
.tenant-id {
@apply text-gray-500 dark:text-gray-400 text-xs;
}
/* 响应式设计 */
@media (max-width: 768px) {
.tenant-badge {
@apply px-2 py-1 text-xs;
}
.tenant-id {
@apply hidden;
}
}
</style>
+26 -15
View File
@@ -1,7 +1,8 @@
import { computed } from 'vue';
import { useAuthStore } from '@/store/modules/auth';
import { useCustomerStore, type CustomerAccess } from '@/store/modules/customer';
import { fetchGetAccessibleCustomers, fetchCustomerIdByOrgId } from '@/service/api/customer';
import { fetchGetAllAvailableCustomers } from '@/service/api/customer/customer';
import { fetchCustomerIdByOrgId } from '@/service/api';
/**
* 客户业务逻辑 Hook
@@ -11,7 +12,7 @@ export function useCustomer() {
const customerStore = useCustomerStore();
// 当前用户角色
const userRole = computed(() => authStore.userInfo?.roles?.[0] || '');
const userRole = computed(() => authStore.userInfo?.roleIds?.[0] || '');
const userInfo = computed(() => authStore.userInfo);
// 是否是 admin 用户
@@ -29,29 +30,38 @@ export function useCustomer() {
if (isAdmin.value) {
// admin 用户:可以切换客户,需要加载所有客户列表
const { data } = await fetchGetAccessibleCustomers();
const { data } = await fetchGetAllAvailableCustomers();
const customerList = (data || []).map(item => ({
id: item.customerId.toString(), // 转换为字符串避免精度问题
name: item.customerName,
code: item.customerId.toString(),
status: 'ACTIVE',
description: `客户ID: ${item.customerId}`
}));
return {
canSwitch: true,
defaultCustomerId: null, // 需要手动选择
customerList: data || []
customerList
};
} else {
// 其他用户:不能切换,使用自己的客户ID
let customerId = user.customerId;
// 如果用户没有直接的 customerId,尝试通过 orgId 获取
if (!customerId && user.orgId && Array.isArray(user.orgId) && user.orgId.length > 0) {
try {
const { data } = await fetchCustomerIdByOrgId(user.orgId[0]);
customerId = data.customerId;
} catch (error) {
console.error('根据组织ID获取客户ID失败:', error);
}
}
// 注意:如果用户类型定义中没有 orgId 字段,这部分逻辑需要调整
// if (!customerId && user.orgId && Array.isArray(user.orgId) && user.orgId.length > 0) {
// try {
// const { data } = await fetchCustomerIdByOrgId(user.orgId[0]);
// customerId = data.customerId;
// } catch (error) {
// console.error('根据组织ID获取客户ID失败:', error);
// }
// }
return {
canSwitch: false,
defaultCustomerId: customerId || 0,
defaultCustomerId: customerId ? customerId.toString() : null, // 转换为字符串
customerList: []
};
}
@@ -72,7 +82,8 @@ export function useCustomer() {
customerStore.setCustomerAccess(access);
// 如果有默认客户ID且当前没有设置,则设置默认值
if (access.defaultCustomerId && !customerStore.currentCustomerId) {
// 注意:对于admin用户,优先使用localStorage中保存的customerId
if (!customerStore.currentCustomerId && access.defaultCustomerId) {
customerStore.setCurrentCustomerId(access.defaultCustomerId);
}
@@ -116,7 +127,7 @@ export function useCustomer() {
/**
* 切换客户(仅超级管理员)
*/
const switchCustomer = async (customerId: number) => {
const switchCustomer = async (customerId: string) => { // 改为字符串类型
if (!customerStore.canSwitchCustomer) {
throw new Error('当前用户无权限切换客户');
}
@@ -9,6 +9,7 @@ import GlobalSearch from '../global-search/index.vue';
import ThemeButton from './components/theme-button.vue';
import UserAvatar from './components/user-avatar.vue';
import CustomerSwitcher from '@/components/common/CustomerSwitcher.vue';
import TenantInfo from '@/components/common/TenantInfo.vue';
defineOptions({
name: 'GlobalHeader'
@@ -44,6 +45,7 @@ const { isFullscreen, toggle } = useFullscreen();
<LangSwitch :lang="appStore.locale" :lang-options="appStore.localeOptions" @change-lang="appStore.changeLocale" />
<ThemeSchemaSwitch :theme-schema="themeStore.themeScheme" :is-dark="themeStore.darkMode" @switch="themeStore.toggleThemeScheme" />
<ThemeButton />
<TenantInfo class="mr-3" />
<CustomerSwitcher class="mr-3" />
<UserAvatar />
</div>
+1
View File
@@ -81,6 +81,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
monitor_system: () => import("@/views/monitor/system/index.vue"),
"multi-menu_first_child": () => import("@/views/multi-menu/first_child/index.vue"),
"multi-menu_second_child_home": () => import("@/views/multi-menu/second_child_home/index.vue"),
"system_customer-select": () => import("@/views/system/customer-select/index.vue"),
"system_dynamic-menu": () => import("@/views/system/dynamic-menu/index.vue"),
system_license: () => import("@/views/system/license/index.vue"),
task: () => import("@/views/task/index.vue"),
+9
View File
@@ -829,6 +829,15 @@ export const generatedRoutes: GeneratedRoute[] = [
i18nKey: 'route.system'
},
children: [
{
name: 'system_customer-select',
path: '/system/customer-select',
component: 'view.system_customer-select',
meta: {
title: 'system_customer-select',
i18nKey: 'route.system_customer-select'
}
},
{
name: 'system_dynamic-menu',
path: '/system/dynamic-menu',
@@ -255,6 +255,7 @@ const routeMap: RouteMap = {
"multi-menu_second_child": "/multi-menu/second/child",
"multi-menu_second_child_home": "/multi-menu/second/child/home",
"system": "/system",
"system_customer-select": "/system/customer-select",
"system_dynamic-menu": "/system/dynamic-menu",
"system_license": "/system/license",
"task": "/task",
@@ -78,4 +78,14 @@ export function fetchRecordCustomerSwitch(data: {
method: 'POST',
data
});
}
// =============== Customer Config APIs ===============
/** 获取所有可用的客户列表(用于客户选择器) */
export function fetchGetAllAvailableCustomers() {
return request<Array<{ customerId: number; customerName: string }>>({
url: '/t_customer_config/all-customers',
method: 'GET'
});
}
@@ -91,10 +91,12 @@ export function fetchDeleteOrgUnits(data: Api.Common.DeleteParams) {
}
/** get org page tree */
export function fetchGetOrgUnitsTree(id: number) {
export function fetchGetOrgUnitsTree(id?: number) {
const params = id ? { id } : {};
return request<Api.SystemManage.OrgUnitsTree[]>({
url: `/sys_org_units/tree?id=${id}`,
method: 'GET'
url: '/sys_org_units/tree',
method: 'GET',
params
});
}
+23 -2
View File
@@ -1,6 +1,7 @@
import type { AxiosResponse } from 'axios';
import { BACKEND_ERROR_CODE, REQUEST_LANGUAGE, createFlatRequest, createRequest } from '@sa/axios';
import { useAuthStore } from '@/store/modules/auth';
import { useCustomerStore } from '@/store/modules/customer';
import { $t } from '@/locales';
import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service';
@@ -28,15 +29,35 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
{
async onRequest(config) {
const authStore = useAuthStore();
const customerStore = useCustomerStore();
const Authorization = getAuthorization();
const language = getLanguage();
// 确定要使用的customerId
let customerId = authStore.userInfo.customerId;
// 如果是admin用户且已选择客户,使用选中的客户ID
if (authStore.userInfo?.userName === 'admin' && customerStore.currentCustomerId) {
customerId = customerStore.currentCustomerId;
}
// 自动添加customerId (排除白名单URL)
const shouldAddCustomerId = authStore.userInfo.customerId !== undefined &&
const shouldAddCustomerId = customerId !== undefined &&
!EXCLUDE_CUSTOMER_ID_URLS.some(excludeUrl => config.url?.includes(excludeUrl));
// 调试信息
if (import.meta.env.DEV) {
console.log('[Request] customerId injection:', {
url: config.url,
method: config.method,
userCustomerId: authStore.userInfo.customerId,
selectedCustomerId: customerStore.currentCustomerId,
finalCustomerId: customerId,
userName: authStore.userInfo?.userName,
shouldAdd: shouldAddCustomerId
});
}
if (shouldAddCustomerId) {
const customerId = authStore.userInfo.customerId;
if (config.method?.toLowerCase() === 'get') {
// GET请求:添加到查询参数
+31 -18
View File
@@ -1,10 +1,11 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { SetupStoreId } from '@/enum';
import { fetchGetCustomerList, fetchCustomerIdByOrgId } from '@/service/api';
import { fetchGetAllAvailableCustomers } from '@/service/api/customer/customer';
import { fetchCustomerIdByOrgId } from '@/service/api';
export interface CustomerInfo {
id: number;
id: string; // 改为字符串类型避免精度问题
name: string;
code?: string;
logo?: string;
@@ -14,16 +15,16 @@ export interface CustomerInfo {
export interface CustomerAccess {
canSwitch: boolean;
defaultCustomerId: number | null;
defaultCustomerId: string | null; // 改为字符串类型
customerList: CustomerInfo[];
}
export const useCustomerStore = defineStore(SetupStoreId.Customer, () => {
// 状态
const currentCustomerId = ref<number | null>(null);
const currentCustomerId = ref<string | null>(null); // 改为字符串类型
const customerList = ref<CustomerInfo[]>([]);
const canSwitchCustomer = ref(false);
const recentCustomers = ref<number[]>([]);
const recentCustomers = ref<string[]>([]); // 改为字符串数组
const loading = ref(false);
// 计算属性
@@ -43,7 +44,7 @@ export const useCustomerStore = defineStore(SetupStoreId.Customer, () => {
});
// Actions
const setCurrentCustomerId = (customerId: number | null) => {
const setCurrentCustomerId = (customerId: string | null) => { // 改为字符串类型
currentCustomerId.value = customerId;
// 更新最近访问记录
@@ -56,7 +57,7 @@ export const useCustomerStore = defineStore(SetupStoreId.Customer, () => {
// 持久化到 localStorage
if (customerId) {
localStorage.setItem('currentCustomerId', customerId.toString());
localStorage.setItem('currentCustomerId', customerId); // 直接存储字符串
localStorage.setItem('recentCustomers', JSON.stringify(recentCustomers.value));
} else {
localStorage.removeItem('currentCustomerId');
@@ -77,14 +78,17 @@ export const useCustomerStore = defineStore(SetupStoreId.Customer, () => {
try {
loading.value = true;
const { data } = await fetchGetCustomerList({
page: 1,
pageSize: 100,
status: 'ACTIVE'
});
const { data } = await fetchGetAllAvailableCustomers();
if (data?.records) {
customerList.value = data.records;
// 转换数据格式
if (data) {
customerList.value = data.map(item => ({
id: item.customerId.toString(), // 转换为字符串避免精度问题
name: item.customerName,
code: item.customerId.toString(),
status: 'ACTIVE', // 接口返回的都是可用的客户
description: `客户ID: ${item.customerId}`
}));
}
} catch (error) {
console.error('加载客户列表失败:', error);
@@ -93,7 +97,7 @@ export const useCustomerStore = defineStore(SetupStoreId.Customer, () => {
}
};
const switchCustomer = async (customerId: number) => {
const switchCustomer = async (customerId: string) => { // 改为字符串类型
if (!canSwitchCustomer.value) {
console.warn('当前用户无权限切换客户');
return;
@@ -102,17 +106,26 @@ export const useCustomerStore = defineStore(SetupStoreId.Customer, () => {
try {
loading.value = true;
console.log('切换客户: customerId =', customerId);
// 设置新的客户ID
setCurrentCustomerId(customerId);
// 清理相关缓存
await clearCustomerRelatedCache();
// 重新加载页面数据
window.location.reload();
// 显示成功消息
const customerName = customerList.value.find(c => c.id === customerId)?.name || customerId;
window.$message?.success(`已切换到客户: ${customerName}`);
// 延迟一下再重新加载页面,让用户看到成功消息
setTimeout(() => {
window.location.reload();
}, 500);
} catch (error) {
console.error('切换客户失败:', error);
window.$message?.error('切换客户失败');
throw error;
} finally {
loading.value = false;
@@ -151,7 +164,7 @@ export const useCustomerStore = defineStore(SetupStoreId.Customer, () => {
const savedRecentCustomers = localStorage.getItem('recentCustomers');
if (savedCustomerId) {
currentCustomerId.value = parseInt(savedCustomerId);
currentCustomerId.value = savedCustomerId; // 直接使用字符串,不进行数字转换
}
if (savedRecentCustomers) {
+2
View File
@@ -12,6 +12,7 @@ declare module 'vue' {
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
CountTo: typeof import('./../components/custom/count-to.vue')['default']
CustomerSelectionPrompt: typeof import('./../components/common/CustomerSelectionPrompt.vue')['default']
CustomerSelector: typeof import('./../components/business/CustomerSelector.vue')['default']
CustomerSwitcher: typeof import('./../components/common/CustomerSwitcher.vue')['default']
DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
@@ -129,6 +130,7 @@ declare module 'vue' {
SystemLogo: typeof import('./../components/common/system-logo.vue')['default']
TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
TenantInfo: typeof import('./../components/common/TenantInfo.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
}
+2
View File
@@ -109,6 +109,7 @@ declare module "@elegant-router/types" {
"multi-menu_second_child": "/multi-menu/second/child";
"multi-menu_second_child_home": "/multi-menu/second/child/home";
"system": "/system";
"system_customer-select": "/system/customer-select";
"system_dynamic-menu": "/system/dynamic-menu";
"system_license": "/system/license";
"task": "/task";
@@ -268,6 +269,7 @@ declare module "@elegant-router/types" {
| "monitor_system"
| "multi-menu_first_child"
| "multi-menu_second_child_home"
| "system_customer-select"
| "system_dynamic-menu"
| "system_license"
| "task"
+45
View File
@@ -0,0 +1,45 @@
import { useAuthStore } from '@/store/modules/auth';
/**
* 检查用户是否有指定权限
* @param permission 权限码,如 'license:management:import'
* @returns boolean
*/
export function hasPermission(permission: string): boolean {
const authStore = useAuthStore();
// 如果没有权限数组,返回 false
if (!authStore.userInfo.permissions || authStore.userInfo.permissions.length === 0) {
return false;
}
// 检查权限数组中是否包含指定权限
return authStore.userInfo.permissions.includes(permission);
}
/**
* 检查用户是否有任意一个权限
* @param permissions 权限码数组
* @returns boolean
*/
export function hasAnyPermission(permissions: string[]): boolean {
return permissions.some(permission => hasPermission(permission));
}
/**
* 检查用户是否拥有所有指定权限
* @param permissions 权限码数组
* @returns boolean
*/
export function hasAllPermissions(permissions: string[]): boolean {
return permissions.every(permission => hasPermission(permission));
}
/**
* 检查用户是否为超级管理员
* @returns boolean
*/
export function isSuperAdmin(): boolean {
const authStore = useAuthStore();
return authStore.isStaticSuper;
}
+11
View File
@@ -21,3 +21,14 @@ export function convertToBeijingTime(timestamp: dayjs.ConfigType) {
export function formatDate(date: dayjs.ConfigType, format: string = 'YYYY-MM-DD') {
return dayjs(date).format(format);
}
/**
* format time
*
* @param time time
* @param format format pattern, default is 'HH:mm:ss'
* @returns formatted time
*/
export function formatTime(time: dayjs.ConfigType, format: string = 'HH:mm:ss') {
return dayjs(time).format(format);
}
+2 -2
View File
@@ -48,8 +48,8 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
page: 1,
pageSize: 20,
name: null,
status: null,
id: authStore.userInfo?.customerId // 使用用户的customerId作为orgId查询参数
status: null
// customerId 将由请求拦截器自动添加
},
columns: () => [
{
@@ -40,7 +40,7 @@ const editingData: Ref<Api.SystemManage.OrgUnits | null> = ref(null);
/** init */
const init = async () => {
const { data, error } = await fetchGetOrgUnitsTree(authStore.userInfo?.customerId);
const { data, error } = await fetchGetOrgUnitsTree();
if (!error && data) {
orgUnitsTreeData.value = data;
}
@@ -0,0 +1,200 @@
<template>
<div class="h-full overflow-hidden">
<NCard title="客户选择" class="h-full shadow-sm">
<div class="space-y-6">
<!-- 当前客户信息 -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 class="text-lg font-medium text-blue-900 mb-2">当前选中客户</h3>
<div v-if="currentCustomer" class="text-blue-700">
<p><span class="font-medium">客户ID:</span> {{ currentCustomer.customerId }}</p>
<p><span class="font-medium">客户名称:</span> {{ currentCustomer.customerName }}</p>
</div>
<div v-else class="text-gray-500">
未选择客户
</div>
</div>
<!-- API 请求信息 -->
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h3 class="text-lg font-medium text-green-900 mb-2">API 请求参数</h3>
<div class="text-green-700">
<p><span class="font-medium">当前用户角色:</span> {{ userInfo?.userName || 'N/A' }}</p>
<p><span class="font-medium">用户原始customerId:</span> {{ userInfo?.customerId || 'N/A' }}</p>
<p><span class="font-medium">选择器选中customerId:</span> {{ customerStore.currentCustomerId || 'N/A' }}</p>
<p><span class="font-medium">实际使用customerId:</span> {{ getEffectiveCustomerId() }}</p>
<p class="text-sm text-green-600 mt-2">
说明:{{ getCustomerIdExplanation() }}
</p>
</div>
</div>
<!-- 客户选择器 -->
<div class="space-y-4">
<h3 class="text-lg font-medium text-gray-900">选择客户</h3>
<div class="flex items-center space-x-4">
<label class="text-sm font-medium text-gray-700 whitespace-nowrap">
客户:
</label>
<div class="flex-1 max-w-md">
<CustomerSelector
ref="customerSelectorRef"
@change="handleCustomerChange"
/>
</div>
</div>
</div>
<!-- 操作说明 */
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">功能说明</h3>
<ul class="text-sm text-gray-600 space-y-1">
<li>• 管理员可以切换不同的客户进行管理</li>
<li>• 选择的客户ID会全局生效,影响后续的数据查询</li>
<li>• 客户信息会保存在本地存储中</li>
<li>• 普通用户只能看到自己所属的客户</li>
</ul>
</div>
<!-- 操作按钮 */
<div class="flex space-x-4">
<NButton type="primary" @click="refreshCustomerList">
<template #icon>
<SvgIcon icon="mdi:refresh" />
</template>
刷新客户列表
</NButton>
<NButton @click="clearSelection">
<template #icon>
<SvgIcon icon="mdi:close" />
</template>
清除选择
</NButton>
<NButton type="info" @click="testApiCall">
<template #icon>
<SvgIcon icon="mdi:api" />
</template>
测试API调用
</NButton>
</div>
<!-- 调试信息 -->
<div v-if="showDebug" class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h3 class="text-lg font-medium text-yellow-900 mb-2">调试信息</h3>
<pre class="text-xs text-yellow-700 overflow-auto">{{ debugInfo }}</pre>
</div>
</div>
</NCard>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { NCard, NButton } from 'naive-ui';
import SvgIcon from '@/components/custom/svg-icon.vue';
import CustomerSelector from '@/components/business/CustomerSelector.vue';
import { useAuthStore } from '@/store/modules/auth';
import { useCustomerStore } from '@/store/modules/customer';
import { fetchGetAllAvailableCustomers } from '@/service/api/customer/customer';
// Store
const authStore = useAuthStore();
const customerStore = useCustomerStore();
// 组件引用
const customerSelectorRef = ref<InstanceType<typeof CustomerSelector>>();
// 当前选中的客户信息
const currentCustomer = ref<{ customerId: string; customerName: string } | null>(null);
// 用户信息
const userInfo = computed(() => authStore.userInfo);
// 是否显示调试信息
const showDebug = ref(false);
// 调试信息
const debugInfo = computed(() => {
return {
currentCustomer: currentCustomer.value,
localStorage: {
selectedCustomerId: localStorage.getItem('selectedCustomerId'),
selectedCustomerName: localStorage.getItem('selectedCustomerName')
},
timestamp: new Date().toISOString()
};
});
// 处理客户切换
const handleCustomerChange = (customerId: string | null, customerName: string | null) => {
console.log('客户切换事件:', { customerId, customerName });
if (customerId && customerName) {
currentCustomer.value = { customerId, customerName };
window.$message?.success(`已切换到客户: ${customerName} (ID: ${customerId})`);
} else {
currentCustomer.value = null;
window.$message?.info('已清除客户选择');
}
};
// 刷新客户列表
const refreshCustomerList = () => {
customerSelectorRef.value?.loadCustomers();
window.$message?.info('正在刷新客户列表...');
};
// 清除选择
const clearSelection = () => {
customerSelectorRef.value?.setSelectedCustomer(null);
currentCustomer.value = null;
};
// 获取实际生效的 customerId
const getEffectiveCustomerId = () => {
const userCustomerId = userInfo.value?.customerId;
const selectedCustomerId = customerStore.currentCustomerId;
if (userInfo.value?.userName === 'admin' && selectedCustomerId) {
return selectedCustomerId;
}
return userCustomerId || 'N/A';
};
// 获取 customerId 使用说明
const getCustomerIdExplanation = () => {
if (userInfo.value?.userName === 'admin') {
if (customerStore.currentCustomerId) {
return 'Admin用户,使用选择器选中的客户ID';
} else {
return 'Admin用户,未选择客户时使用用户原始ID';
}
} else {
return '普通用户,使用用户自身的客户ID';
}
};
// 测试API调用
const testApiCall = async () => {
try {
console.log('测试API调用开始');
const response = await fetchGetAllAvailableCustomers();
console.log('API响应:', response);
window.$message?.success(`API调用成功,返回${response.data?.length || 0}个客户`);
} catch (error) {
console.error('API调用失败:', error);
window.$message?.error('API调用失败');
}
};
// 开发模式下显示调试信息
if (import.meta.env.DEV) {
showDebug.value = true;
}
</script>
<style scoped>
/* 可以添加一些自定义样式 */
</style>
@@ -53,7 +53,9 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Controller 控制层
@@ -365,5 +367,35 @@ public class TCustomerConfigController {
return Result.failure("更新失败: " + e.getMessage());
}
}
/**
* 获取所有可用的客户列表(用于客户选择器)
*/
@GetMapping("/all-customers")
@Operation(operationId = "12", summary = "获取所有可用的客户列表")
public Result<List<Map<String, Object>>> getAllAvailableCustomers() {
try {
List<TCustomerConfig> customerConfigs = tCustomerConfigService.list();
List<Map<String, Object>> customers = customerConfigs.stream()
.filter(config -> config.getIsDeleted() == null || config.getIsDeleted() == 0) // 过滤未删除的
.map(config -> {
Map<String, Object> customer = new HashMap<>();
customer.put("customerId", config.getId());
customer.put("customerName", config.getCustomerName());
log.debug("Customer: id={}, name={}, isDeleted={}",
config.getId(), config.getCustomerName(), config.getIsDeleted());
return customer;
})
.collect(Collectors.toList());
log.info("获取可用客户列表成功,共{}个客户", customers.size());
return Result.data(customers);
} catch (Exception e) {
log.error("获取可用客户列表失败", e);
return Result.failure("获取客户列表失败: " + e.getMessage());
}
}
}
@@ -54,6 +54,7 @@ public class SysOrgUnitsController {
System.out.println(" id: " + sysOrgUnitsSearchDTO.getId());
System.out.println(" name: " + sysOrgUnitsSearchDTO.getName());
System.out.println(" status: " + sysOrgUnitsSearchDTO.getStatus());
System.out.println(" customerId: " + sysOrgUnitsSearchDTO.getCustomerId());
return Result.data(sysOrgUnitsFacade.listSysOrgUnitsPage(pageQuery, sysOrgUnitsSearchDTO));
}
@@ -93,8 +94,17 @@ public class SysOrgUnitsController {
@GetMapping("/tree")
@SaCheckPermission("sys:org:units:tree")
@Operation(operationId = "6", summary = "获取租户/部门树结构数据")
public Result<List<SysOrgUnitsTreeVO>> tree(@RequestParam(value = "id", required = false) Long id) {
return Result.data(sysOrgUnitsFacade.queryAllOrgUnitsListConvertToTree(id));
public Result<List<SysOrgUnitsTreeVO>> tree(@RequestParam(value = "id", required = false) Long id,
@RequestParam(value = "customerId", required = false) Long customerId) {
System.out.println("🔍 SysOrgUnitsController.tree - 接收到参数:");
System.out.println(" id: " + id);
System.out.println(" customerId: " + customerId);
// 如果传入了customerId,说明要按租户过滤,将customerId作为id进行查询
Long effectiveId = (customerId != null) ? customerId : id;
System.out.println(" effectiveId: " + effectiveId);
return Result.data(sysOrgUnitsFacade.queryAllOrgUnitsListConvertToTree(effectiveId));
}
@PostMapping("/delete-precheck")
@@ -52,4 +52,7 @@ public class SysOrgUnitsSearchDTO implements Serializable {
@Schema(description = "ID")
private Long id;
@Schema(description = "客户ID")
private Long customerId;
}
@@ -107,6 +107,12 @@ public class SysOrgUnitsFacadeImpl implements ISysOrgUnitsFacade {
@Override
public RPage<SysOrgUnitsTreeVO> listSysOrgUnitsPage(PageQuery pageQuery, SysOrgUnitsSearchDTO sysOrgUnitsSearchDTO) {
SysOrgUnitsBO sysOrgUnitsBO = CglibUtil.convertObj(sysOrgUnitsSearchDTO, SysOrgUnitsBO::new);
// 如果传入了customerId,说明要按租户过滤,将customerId作为id进行查询
if (sysOrgUnitsSearchDTO.getCustomerId() != null) {
System.out.println("🔍 SysOrgUnitsFacadeImpl - 根据customerId过滤: " + sysOrgUnitsSearchDTO.getCustomerId());
sysOrgUnitsBO.setId(sysOrgUnitsSearchDTO.getCustomerId());
}
IPage<SysOrgUnits> sysOrgUnitsIPage = sysOrgUnitsService.listSysOrgUnitsPage(pageQuery, sysOrgUnitsBO);
List<SysOrgUnits> topOrgUnits = sysOrgUnitsIPage.getRecords();
if (topOrgUnits.isEmpty()) {
@@ -102,7 +102,9 @@ public class OrgUnitsChangeListener {
interfaceService.remove(new QueryWrapper<TInterface>().eq("customer_id", o.getId())); // 先清理
List<TInterface> exists = interfaceService.list(new QueryWrapper<TInterface>().eq("customer_id", o.getId()));
Set<String> existNames = exists.stream().map(TInterface::getName).collect(Collectors.toSet());
interfaceService.list(new QueryWrapper<TInterface>().eq("customer_id", 1).eq("is_deleted", 0)).stream()
List<TInterface> templateInterfaces = interfaceService.list(new QueryWrapper<TInterface>().eq("customer_id", 0).eq("is_deleted", 0));
log.warn("Found {} template interfaces to clone", templateInterfaces.size());
templateInterfaces.stream()
.filter(i -> !existNames.contains(i.getName()))
.forEach(i -> {
log.warn("Inserting interface name={} for orgId={}", i.getName(), o.getId());
@@ -113,7 +115,7 @@ public class OrgUnitsChangeListener {
TInterface n = new TInterface();
n.setName(i.getName()); n.setUrl(i.getUrl()); n.setCallInterval(i.getCallInterval());
n.setMethod(i.getMethod()); n.setDescription(i.getDescription()); n.setEnabled(i.getEnabled());
n.setCustomerId(o.getId()); n.setDeleted(0); n.setCreateTime(i.getCreateTime()); n.setUpdateTime(i.getUpdateTime());
n.setCustomerId(o.getId()); n.setIsDeleted(0); n.setCreateTime(i.getCreateTime()); n.setUpdateTime(i.getUpdateTime());
n.setApiAuth(i.getApiAuth());
n.setApiId(i.getApiId());
interfaceService.saveOrUpdate(n);
@@ -123,7 +125,10 @@ public class OrgUnitsChangeListener {
private void cloneHealthDataConfig(SysOrgUnits o) { // 复制健康数据配置
log.warn("cloneHealthDataConfig called for orgId={}", o.getId());
healthDataConfigService.remove(new QueryWrapper<THealthDataConfig>().eq("customer_id", o.getId())); // 先清理
healthDataConfigService.list(new QueryWrapper<THealthDataConfig>().eq("customer_id", 1)).forEach(h -> {
List<THealthDataConfig> templateConfigs = healthDataConfigService.list(new QueryWrapper<THealthDataConfig>().eq("customer_id", 0).eq("is_deleted", 0));
log.warn("Found {} template health data configs to clone", templateConfigs.size());
templateConfigs.forEach(h -> {
log.warn("Cloning health data config: dataType={} for orgId={}", h.getDataType(), o.getId());
THealthDataConfig n = new THealthDataConfig();
n.setCustomerId(o.getId()); n.setDataType(h.getDataType()); n.setFrequencyInterval(h.getFrequencyInterval());
n.setIsRealtime(h.getIsRealtime()); n.setIsEnabled(h.getIsEnabled()); n.setIsDefault(h.getIsDefault());
+6 -6
View File
@@ -2,12 +2,12 @@
# 更新时间: 2025-06-26
# 核心应用版本
LJWX_VERSION=2.0.2
LJWX_MYSQL_VERSION=2.0.2
LJWX_REDIS_VERSION=2.0.2
LJWX_BOOT_VERSION=2.0.2
LJWX_BIGSCREEN_VERSION=2.0.2
LJWX_ADMIN_VERSION=2.0.2
LJWX_VERSION=2.0.3
LJWX_MYSQL_VERSION=2.0.3
LJWX_REDIS_VERSION=2.0.3
LJWX_BOOT_VERSION=2.0.3
LJWX_BIGSCREEN_VERSION=2.0.3
LJWX_ADMIN_VERSION=2.0.3
# 定制化监控组件版本
LJWX_GRAFANA_VERSION=1.2.6