fix(core): 统一错误处理机制并增强租户管理员角色权限控制

- 移除页面中手动 message.error 调用,统一由拦截器处理
- 在 http 拦截器中实现后端错误消息自动展示
- 添加对业务错误码(code != 0)的处理逻辑
- 完善 HTTP 状态码错误(4xx, 5xx)的处理
- 增强 RoleController 中租户管理员角色的越权保护
- 在角色权限绑定页面添加平台管理员模式检查
- 限制非平台管理员修改 TENANT_ADMIN 角色权限的能力
- 清理调试用的 console.log 输出
master
chenhao 2026-02-26 17:09:14 +08:00
parent 0530605839
commit bf7439b200
14 changed files with 68 additions and 48 deletions

View File

@ -101,6 +101,18 @@ public class RoleController {
// 权限越权校验
Long currentTenantId = getCurrentTenantId();
SysRole targetRole = sysRoleService.getById(id);
if (targetRole == null) {
return ApiResponse.error("角色不存在");
}
// 关键校验:只有平台管理员可以修改 TENANT_ADMIN 角色的权限
if ("TENANT_ADMIN".equalsIgnoreCase(targetRole.getRoleCode())) {
if (!Long.valueOf(0).equals(currentTenantId)) {
return ApiResponse.error("租户管理员角色的权限只能由平台管理员修改");
}
}
if (!Long.valueOf(0).equals(currentTenantId)) {
List<com.imeeting.entity.SysPermission> myPerms = sysPermissionService.listByUserId(getCurrentUserId(), currentTenantId);

View File

@ -1,4 +1,5 @@
import axios from "axios";
import { message } from "antd";
const http = axios.create({
baseURL: "",
@ -17,8 +18,11 @@ http.interceptors.request.use((config) => {
http.interceptors.response.use(
(resp) => {
const body = resp.data;
// 如果返回的 code 不是 0表示业务错误
if (body && body.code !== "0") {
const err = new Error(body.msg || "请求失败");
const errorMsg = body.msg || "请求失败";
message.error(errorMsg); // 自动展示后端错误消息
const err = new Error(errorMsg);
(err as any).code = body.code;
(err as any).msg = body.msg;
return Promise.reject(err);
@ -26,17 +30,21 @@ http.interceptors.response.use(
return resp;
},
(error) => {
// 处理 HTTP 状态码错误 (4xx, 5xx)
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
// Clear session/local storage
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
sessionStorage.removeItem("userProfile");
// Force redirect to login with timeout flag
window.location.href = "/login?timeout=1";
return Promise.reject(error);
}
// Process backend error message if available in response body even for non-200 status
const body = error.response?.data;
const errorMsg = body?.msg || error.message || "网络异常";
// 防止重复弹出相同的提示(可选逻辑,根据需要调整)
message.error(errorMsg);
if (body && body.msg) {
const err = new Error(body.msg);
(err as any).code = body.code;

View File

@ -74,7 +74,6 @@ export function usePermission() {
if (isAdmin) return true;
if (!codes || codes.length === 0) {
console.log(`Permission check for [${perm}]: codes is empty, returning true`);
return true;
}
@ -87,8 +86,6 @@ export function usePermission() {
} else {
result = !hasButtonCodes || codes.includes(perm);
}
console.log(`Permission check for [${perm}]: result=[${result}], hasMenuCodes=[${hasMenuCodes}], hasButtonCodes=[${hasButtonCodes}]`);
return result;
};

View File

@ -52,7 +52,7 @@ export default function Devices() {
setData(deviceList || []);
setUsers(usersList || []);
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
} finally {
setLoading(false);
}
@ -113,7 +113,7 @@ export default function Devices() {
setOpen(false);
loadData();
} catch (e) {
if (e instanceof Error && e.message) message.error(e.message);
// Handled by interceptor
} finally {
setSaving(false);
}
@ -125,7 +125,7 @@ export default function Devices() {
message.success(t('common.success'));
loadData();
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
};

View File

@ -25,9 +25,9 @@ export default function Login() {
const data = await fetchCaptcha();
setCaptcha(data);
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
}, [captchaEnabled, t]);
}, [captchaEnabled]);
useEffect(() => {
const init = async () => {
@ -91,7 +91,6 @@ export default function Login() {
message.success(t('common.success'));
window.location.href = "/";
} catch (e: any) {
message.error(e.message || t('common.error'));
if (captchaEnabled) {
loadCaptcha();
}

View File

@ -93,7 +93,7 @@ export default function Orgs() {
setSelectedTenantId(list[0].id);
}
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
};
@ -104,7 +104,7 @@ export default function Orgs() {
const list = await listOrgs(selectedTenantId);
setData(list || []);
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
} finally {
setLoading(false);
}
@ -148,7 +148,7 @@ export default function Orgs() {
message.success(t('common.success'));
loadOrgs();
} catch (e: any) {
message.error(e.message || t('common.error'));
// Handled by interceptor
}
};
@ -166,7 +166,7 @@ export default function Orgs() {
setDrawerOpen(false);
loadOrgs();
} catch (e) {
if (e instanceof Error && e.message) message.error(e.message);
// Handled by interceptor
} finally {
setSaving(false);
}

View File

@ -161,7 +161,7 @@ export default function Permissions() {
setOpen(false);
load();
} catch (e) {
if (e instanceof Error && e.message) message.error(e.message);
// Handled by interceptor
} finally {
setSaving(false);
}
@ -173,7 +173,7 @@ export default function Permissions() {
message.success(t('common.success'));
load();
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
};

View File

@ -37,7 +37,7 @@ export default function PlatformSettings() {
const data = await getAdminPlatformConfig();
form.setFieldsValue(data);
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
} finally {
setLoading(false);
}
@ -53,7 +53,7 @@ export default function PlatformSettings() {
form.setFieldValue(fieldName, url);
message.success(t('common.success'));
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
return false; // 阻止自动上传
};
@ -64,7 +64,7 @@ export default function PlatformSettings() {
await updatePlatformConfig(values);
message.success(t('common.success'));
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
} finally {
setSaving(false);
}

View File

@ -74,6 +74,16 @@ export default function RolePermissionBinding() {
const [saving, setSaving] = useState(false);
const [selectedRoleId, setSelectedRoleId] = useState<number | null>(null);
// Platform admin check
const isPlatformMode = useMemo(() => {
const profileStr = sessionStorage.getItem("userProfile");
if (profileStr) {
const profile = JSON.parse(profileStr);
return profile.isPlatformAdmin && localStorage.getItem("activeTenantId") === "0";
}
return false;
}, []);
// Selection states
const [checkedPermIds, setCheckedPermIds] = useState<number[]>([]);
const [halfCheckedIds, setHalfCheckedIds] = useState<number[]>([]);
@ -102,7 +112,7 @@ export default function RolePermissionBinding() {
const list = await listPermissions();
setPermissions(list || []);
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
} finally {
setLoadingPerms(false);
}
@ -121,7 +131,6 @@ export default function RolePermissionBinding() {
setHalfCheckedIds([]);
} catch (e) {
setCheckedPermIds([]);
message.error(t('common.error'));
}
};
@ -161,7 +170,7 @@ export default function RolePermissionBinding() {
await saveRolePermissions(selectedRoleId, allPermIds);
message.success(t('common.success'));
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
} finally {
setSaving(false);
}
@ -179,7 +188,7 @@ export default function RolePermissionBinding() {
icon={<SaveOutlined aria-hidden="true" />}
onClick={handleSave}
loading={saving}
disabled={!selectedRoleId}
disabled={!selectedRoleId || (selectedRole?.roleCode === 'TENANT_ADMIN' && !isPlatformMode)}
>
{saving ? t('common.loading') : t('rolePerm.savePolicy')}
</Button>
@ -261,7 +270,7 @@ export default function RolePermissionBinding() {
onCheck={(keys, info) => {
const checked = Array.isArray(keys) ? keys : keys.checked;
const halfChecked = info.halfCheckedKeys || [];
setSelectedPermIds(checked.map(k => Number(k)));
setCheckedPermIds(checked.map(k => Number(k)));
setHalfCheckedIds(halfChecked.map(k => Number(k)));
}}
defaultExpandAll

View File

@ -182,7 +182,7 @@ export default function Roles() {
setUserModalOpen(false);
selectRole(selectedRole);
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
};
@ -193,7 +193,7 @@ export default function Roles() {
message.success(t('common.success'));
selectRole(selectedRole);
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
};
@ -259,7 +259,7 @@ export default function Roles() {
const users = await fetchUsersByRoleId(role.roleId);
setRoleUsers(users || []);
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
} finally {
setLoadingUsers(false);
}
@ -315,7 +315,7 @@ export default function Roles() {
if (selectedRole?.roleId === id) setSelectedRole(null);
loadRoles();
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
};
@ -342,7 +342,7 @@ export default function Roles() {
setDrawerOpen(false);
loadRoles();
} catch (e) {
if (e instanceof Error && e.message) message.error(e.message);
// Handled by interceptor
} finally {
setSaving(false);
}
@ -356,7 +356,7 @@ export default function Roles() {
await saveRolePermissions(selectedRole.roleId, allPermIds);
message.success(t('common.success'));
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
} finally {
setSaving(false);
}
@ -473,7 +473,7 @@ export default function Roles() {
icon={<SaveOutlined aria-hidden="true" />}
loading={saving}
onClick={savePermissions}
disabled={!can("sys:role:permission:save")}
disabled={!can("sys:role:permission:save") || (selectedRole.roleCode === 'TENANT_ADMIN' && !isPlatformMode)}
>
{t('roles.savePerms')}
</Button>

View File

@ -64,12 +64,10 @@ export default function SysParams() {
const res = await pageParams(query);
setData(res.records || []);
setTotal(res.total || 0);
} catch (e) {
message.error(t('common.error'));
} finally {
setLoading(false);
}
}, [queryParams, t]);
}, [queryParams]);
useEffect(() => {
loadData();
@ -109,7 +107,7 @@ export default function SysParams() {
message.success(t('common.success'));
loadData();
} catch (e) {
message.error(t('common.error'));
// Handled by global interceptor
}
};

View File

@ -59,8 +59,6 @@ export default function Tenants() {
const result = await listTenants(currentParams);
setData(result.records || []);
setTotal(result.total || 0);
} catch (e) {
message.error(t('common.error'));
} finally {
setLoading(false);
}
@ -108,7 +106,7 @@ export default function Tenants() {
message.success(t('common.success'));
loadData();
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
};
@ -131,7 +129,7 @@ export default function Tenants() {
setDrawerOpen(false);
loadData();
} catch (e) {
if (e instanceof Error && e.message) message.error(e.message);
// Handled by interceptor
} finally {
setSaving(false);
}

View File

@ -64,7 +64,6 @@ export default function UserRoleBinding() {
setCheckedRoleIds(list || []);
} catch (e) {
setCheckedRoleIds([]);
message.error(t('common.error'));
}
};
@ -100,7 +99,7 @@ export default function UserRoleBinding() {
await saveUserRoles(selectedUserId, checkedRoleIds);
message.success(t('common.success'));
} catch (e) {
message.error(t('common.error'));
// Handled by global interceptor
} finally {
setSaving(false);
}

View File

@ -145,7 +145,7 @@ export default function Users() {
setTenants(tenantsResp.records || []);
}
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
};
@ -230,7 +230,7 @@ export default function Users() {
});
setDrawerOpen(true);
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
};
@ -240,7 +240,7 @@ export default function Users() {
message.success(t('common.success'));
loadUsersData();
} catch (e) {
message.error(t('common.error'));
// Handled by interceptor
}
};
@ -287,7 +287,7 @@ export default function Users() {
setDrawerOpen(false);
loadUsersData();
} catch (e) {
if (e instanceof Error && e.message) message.error(e.message);
// Handled by interceptor
} finally {
setSaving(false);
}