From 70e739818a06ac25c4cbee67b7b3639b8fd68edf Mon Sep 17 00:00:00 2001 From: chenhao Date: Tue, 3 Mar 2026 10:08:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(hotword):=20=E5=AE=9E=E7=8E=B0=E7=83=AD?= =?UTF-8?q?=E8=AF=8D=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86=E5=92=8C=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加管理员权限判断方法支持平台管理员和租户管理员 - 实现公开热词仅管理员可创建、修改、删除的权限控制 - 支持私有热词本人或管理员可操作的权限验证 - 添加前端搜索类型筛选功能支持公开/私有分类 - 更新前端权限检查逻辑统一使用管理员身份判断 - 修复后端查询构造器逻辑确保权限过滤正确执行 - 添加租户管理员身份到JWT认证信息中 - 配置数据访问层忽略租户隔离注解优化查询性能 --- .../auth/JwtAuthenticationFilter.java | 2 +- .../controller/biz/HotWordController.java | 75 +++++++++++++------ .../imeeting/mapper/SysUserRoleMapper.java | 2 + frontend/src/api/business/hotword.ts | 4 +- frontend/src/pages/business/HotWords.tsx | 47 +++++++----- 5 files changed, 86 insertions(+), 44 deletions(-) diff --git a/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java b/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java index 8f4ba51..85c6772 100644 --- a/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java +++ b/backend/src/main/java/com/imeeting/auth/JwtAuthenticationFilter.java @@ -140,7 +140,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } boolean isTenantAdmin = authScopeService.isTenantAdmin(userId, activeTenantId); - LoginUser loginUser = new LoginUser(userId, activeTenantId, username, user.getIsPlatformAdmin(), isTenantAdmin, permissions); + LoginUser loginUser = new LoginUser(userId, activeTenantId, username, displayName,user.getIsPlatformAdmin(), isTenantAdmin, permissions); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); diff --git a/backend/src/main/java/com/imeeting/controller/biz/HotWordController.java b/backend/src/main/java/com/imeeting/controller/biz/HotWordController.java index 8f4fb5d..a7cd3dd 100644 --- a/backend/src/main/java/com/imeeting/controller/biz/HotWordController.java +++ b/backend/src/main/java/com/imeeting/controller/biz/HotWordController.java @@ -26,17 +26,23 @@ public class HotWordController { this.hotWordService = hotWordService; } + /** + * 判断当前用户是否具备管理员权限 (平台管理员或租户管理员) + */ + private boolean isCurrentUserAdmin(LoginUser user) { + return Boolean.TRUE.equals(user.getIsPlatformAdmin()) || Boolean.TRUE.equals(user.getIsTenantAdmin()); + } + @PostMapping @PreAuthorize("isAuthenticated()") public ApiResponse save(@RequestBody HotWordDTO hotWordDTO) { LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - // 只有管理员可以创建公开热词,普通用户强制为私有 - if (Integer.valueOf(1).equals(hotWordDTO.getIsPublic()) && !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { - // 这里根据需求:租户内管理员可编辑。由于目前权限模型暂未细化到租户管理员字段, - // 我们暂定具有 ADMIN 角色或 PlatformAdmin 的为管理员。 - // 简单处理:普通用户强制设为 0。 - hotWordDTO.setIsPublic(0); + + // 核心校验:只有管理员可以创建公开热词 + if (Integer.valueOf(1).equals(hotWordDTO.getIsPublic()) && !isCurrentUserAdmin(loginUser)) { + return ApiResponse.error("无权创建租户公开热词,请设为个人私有"); } + return ApiResponse.ok(hotWordService.saveHotWord(hotWordDTO, loginUser.getUserId())); } @@ -47,13 +53,20 @@ public class HotWordController { HotWord existing = hotWordService.getById(hotWordDTO.getId()); if (existing == null) return ApiResponse.error("热词不存在"); - // 权限校验:公开热词仅管理员可改,私有热词仅本人可改 + boolean isAdmin = isCurrentUserAdmin(loginUser); + + // 核心校验逻辑: + // 1. 如果用户尝试将热词设为公开,必须具备管理员权限 + if (Integer.valueOf(1).equals(hotWordDTO.getIsPublic()) && !isAdmin) { + return ApiResponse.error("无权将热词设为公开"); + } + + // 2. 如果是公开热词,只有管理员能改 if (Integer.valueOf(1).equals(existing.getIsPublic())) { - if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { - return ApiResponse.error("无权修改公开热词"); - } + if (!isAdmin) return ApiResponse.error("无权修改公开热词"); } else { - if (!existing.getCreatorId().equals(loginUser.getUserId())) { + // 3. 如果是私有热词,本人或管理员能改 + if (!existing.getCreatorId().equals(loginUser.getUserId()) && !isAdmin) { return ApiResponse.error("无权修改他人私有热词"); } } @@ -68,12 +81,13 @@ public class HotWordController { HotWord existing = hotWordService.getById(id); if (existing == null) return ApiResponse.ok(true); + boolean isAdmin = isCurrentUserAdmin(loginUser); + + // 权限校验:公开热词管理员可删,私有热词本人或管理员可删 if (Integer.valueOf(1).equals(existing.getIsPublic())) { - if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { - return ApiResponse.error("无权删除公开热词"); - } + if (!isAdmin) return ApiResponse.error("无权删除公开热词"); } else { - if (!existing.getCreatorId().equals(loginUser.getUserId())) { + if (!existing.getCreatorId().equals(loginUser.getUserId()) && !isAdmin) { return ApiResponse.error("无权删除他人私有热词"); } } @@ -87,20 +101,33 @@ public class HotWordController { @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String word, - @RequestParam(required = false) String category) { + @RequestParam(required = false) String category, + @RequestParam(required = false) Integer isPublic) { LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + boolean isAdmin = isCurrentUserAdmin(loginUser); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(HotWord::getTenantId, loginUser.getTenantId()); + + if (!isAdmin) { + // 普通用户:只能看到“已公开”的,或者“自己创建”的 + wrapper.and(w -> w.eq(HotWord::getIsPublic, 1).or().eq(HotWord::getCreatorId, loginUser.getUserId())); + } - Page page = hotWordService.page(new Page<>(current, size), - new LambdaQueryWrapper() - .eq(HotWord::getTenantId, loginUser.getTenantId()) - .and(w -> w.eq(HotWord::getCreatorId, loginUser.getUserId()).or().eq(HotWord::getIsPublic, 1)) - .like(word != null && !word.isEmpty(), HotWord::getWord, word) - .eq(category != null && !category.isEmpty(), HotWord::getCategory, category) - .orderByDesc(HotWord::getIsPublic) // 公开的排在前面 - .orderByDesc(HotWord::getCreatedAt)); + // 增加类型过滤 + if (isPublic != null) { + wrapper.eq(HotWord::getIsPublic, isPublic); + } + + wrapper.like(word != null && !word.isEmpty(), HotWord::getWord, word) + .eq(category != null && !category.isEmpty(), HotWord::getCategory, category) + .orderByDesc(HotWord::getIsPublic) + .orderByDesc(HotWord::getCreatedAt); + Page page = hotWordService.page(new Page<>(current, size), wrapper); List vos = page.getRecords().stream().map(this::toVO).collect(Collectors.toList()); + PageResult> result = new PageResult<>(); result.setTotal(page.getTotal()); result.setRecords(vos); diff --git a/backend/src/main/java/com/imeeting/mapper/SysUserRoleMapper.java b/backend/src/main/java/com/imeeting/mapper/SysUserRoleMapper.java index e6766d3..815f771 100644 --- a/backend/src/main/java/com/imeeting/mapper/SysUserRoleMapper.java +++ b/backend/src/main/java/com/imeeting/mapper/SysUserRoleMapper.java @@ -1,5 +1,6 @@ package com.imeeting.mapper; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.imeeting.entity.SysUserRole; import org.apache.ibatis.annotations.Param; @@ -10,6 +11,7 @@ import java.util.List; @Mapper public interface SysUserRoleMapper extends BaseMapper { + @InterceptorIgnore(tenantLine = "true") @Select(""" SELECT COUNT(1) FROM sys_user_role ur diff --git a/frontend/src/api/business/hotword.ts b/frontend/src/api/business/hotword.ts index 857d9b4..bd90041 100644 --- a/frontend/src/api/business/hotword.ts +++ b/frontend/src/api/business/hotword.ts @@ -31,8 +31,8 @@ export const getHotWordPage = (params: { current: number; size: number; word?: string; - category?: string; - matchStrategy?: number + category?: string; + isPublic?: number; }) => { return http.get( "/api/biz/hotword/page", diff --git a/frontend/src/pages/business/HotWords.tsx b/frontend/src/pages/business/HotWords.tsx index 7db5739..07bbebb 100644 --- a/frontend/src/pages/business/HotWords.tsx +++ b/frontend/src/pages/business/HotWords.tsx @@ -24,7 +24,6 @@ const { Text } = Typography; const HotWords: React.FC = () => { const [form] = Form.useForm(); - const [searchForm] = Form.useForm(); const { items: categories, loading: dictLoading } = useDict('biz_hotword_category'); const [loading, setLoading] = useState(false); const [data, setData] = useState([]); @@ -33,6 +32,7 @@ const HotWords: React.FC = () => { const [size, setSize] = useState(10); const [searchWord, setSearchWord] = useState(''); const [searchCategory, setSearchCategory] = useState(undefined); + const [searchType, setSearchType] = useState(undefined); const [modalVisible, setModalVisible] = useState(false); const [editingId, setEditingId] = useState(null); @@ -44,11 +44,14 @@ const HotWords: React.FC = () => { return profileStr ? JSON.parse(profileStr) : {}; }, []); - const isPlatformAdmin = userProfile.isPlatformAdmin === true; + // 判定是否具有管理员权限 (平台管理员或租户管理员) + const isAdmin = React.useMemo(() => { + return userProfile.isPlatformAdmin === true || userProfile.isTenantAdmin === true; + }, [userProfile]); useEffect(() => { fetchData(); - }, [current, size, searchWord, searchCategory]); + }, [current, size, searchWord, searchCategory, searchType]); const fetchData = async () => { setLoading(true); @@ -57,7 +60,8 @@ const HotWords: React.FC = () => { current, size, word: searchWord, - category: searchCategory + category: searchCategory, + isPublic: searchType }); if (res.data && res.data.data) { setData(res.data.data.records); @@ -72,8 +76,7 @@ const HotWords: React.FC = () => { const handleOpenModal = (record?: HotWordVO) => { if (record) { - // 权限校验 - if (record.isPublic === 1 && !isPlatformAdmin) { + if (record.isPublic === 1 && !isAdmin) { message.error('公开热词仅限管理员修改'); return; } @@ -142,9 +145,9 @@ const HotWords: React.FC = () => { {text} {record.isPublic === 1 ? ( - + ) : ( - + )} ) @@ -166,7 +169,7 @@ const HotWords: React.FC = () => { render: (val: string) => categories.find(i => i.itemValue === val)?.itemLabel || val }, { - title: '类型', + title: '范围', dataIndex: 'isPublic', key: 'isPublic', render: (val: number) => val === 1 ? 公开 : 私有 @@ -181,7 +184,8 @@ const HotWords: React.FC = () => { title: '操作', key: 'action', render: (_: any, record: HotWordVO) => { - const canEdit = record.isPublic === 1 ? isPlatformAdmin : record.creatorId === userProfile.userId; + const isMine = record.creatorId === userProfile.userId; + const canEdit = record.isPublic === 1 ? isAdmin : (isMine || isAdmin); return ( {canEdit ? ( @@ -205,12 +209,21 @@ const HotWords: React.FC = () => { + + @@ -219,7 +232,7 @@ const HotWords: React.FC = () => { prefix={} allowClear onPressEnter={(e) => {setSearchWord((e.target as any).value); setCurrent(1);}} - style={{ width: 200 }} + style={{ width: 180 }} />