# 权限注解转换指南 ## 目标 将 urbanLifeline 的权限体系完全迁移到 pigx 平台的权限模型。 ## 核心概念对比 | 特性 | urbanLifeline | pigx | |------|---------------|------| | 权限注解 | `@PreAuthorize("hasAuthority()")` | `@PreAuthorize("@pms.hasPermission()")` | | 权限格式 | `module:resource:action` | `module_resource_action` | | 用户获取 | `JwtUtils.getUserId()` | `SecurityUtils.getUser()` | | 用户服务 | `UserService` (本地) | `RemoteUserService` (Feign) | | 响应格式 | `ResultDomain` | `R` | | 租户支持 | 无 | 有 (tenant_id) | ## 转换步骤详解 ### 步骤 1:权限注解转换 #### 1.1 基本转换规则 ```java // ❌ 旧代码 (urbanLifeline) @PreAuthorize("hasAuthority('workcase:ticket:create')") public ResultDomain createWorkcase(@RequestBody TbWorkcaseDTO dto) { // ... } // ✅ 新代码 (pigx) @PreAuthorize("@pms.hasPermission('workcase_ticket_add')") public R createWorkcase(@RequestBody TbWorkcaseDTO dto) { // ... } ``` #### 1.2 多权限组合 ```java // ❌ 旧代码 - OR 条件 @PreAuthorize("hasAuthority('workcase:ticket:update') or hasAuthority('workcase:ticket:admin')") // ✅ 新代码 - OR 条件 @PreAuthorize("@pms.hasPermission('workcase_ticket_edit') or @pms.hasPermission('workcase_ticket_admin')") // ❌ 旧代码 - AND 条件 @PreAuthorize("hasAuthority('workcase:ticket:view') and hasAuthority('workcase:export:data')") // ✅ 新代码 - AND 条件 @PreAuthorize("@pms.hasPermission('workcase_ticket_view') and @pms.hasPermission('workcase_export_data')") ``` #### 1.3 动态权限检查 ```java // ❌ 旧代码 @Service public class WorkcaseService { public boolean canEdit(Long workcaseId) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); return auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals("workcase:ticket:update")); } } // ✅ 新代码 @Service public class WorkcaseService { @Autowired private PermissionService permissionService; public boolean canEdit(Long workcaseId) { return permissionService.hasPermission("workcase_ticket_edit"); } } ``` ### 步骤 2:用户信息获取转换 #### 2.1 获取当前用户 ```java // ❌ 旧代码 Long userId = JwtUtils.getUserId(); String username = JwtUtils.getUsername(); String role = JwtUtils.getRole(); // ✅ 新代码 PigxUser user = SecurityUtils.getUser(); Long userId = user.getId(); String username = user.getUsername(); Long tenantId = user.getTenantId(); // 新增:租户ID Long deptId = user.getDeptId(); // 新增:部门ID List roles = user.getRoles(); // 角色列表 ``` #### 2.2 Service 层用户信息处理 ```java // ❌ 旧代码 @Service public class WorkcaseServiceImpl implements WorkcaseService { @Autowired private UserService userService; public void assignWorkcase(Long workcaseId, Long assigneeId) { User assignee = userService.getById(assigneeId); // 处理逻辑... } } // ✅ 新代码 @Service public class WorkcaseServiceImpl implements WorkcaseService { @Autowired private RemoteUserService remoteUserService; public void assignWorkcase(Long workcaseId, Long assigneeId) { // 使用 Feign 远程调用 R result = remoteUserService.selectById(assigneeId); if (result.isSuccess() && result.getData() != null) { SysUser assignee = result.getData(); // 处理逻辑... } else { throw new BusinessException("用户不存在"); } } } ``` ### 步骤 3:响应格式转换 #### 3.1 成功响应 ```java // ❌ 旧代码 return ResultDomain.success(data); return ResultDomain.success(list, total); return ResultDomain.success("操作成功", data); // ✅ 新代码 return R.ok(data); return R.ok(list, total); // 分页响应 return R.ok(data, "操作成功"); ``` #### 3.2 错误响应 ```java // ❌ 旧代码 return ResultDomain.failure("参数错误"); return ResultDomain.failure(ErrorCode.INVALID_PARAM); throw new BusinessException("业务异常"); // ✅ 新代码 return R.failed("参数错误"); return R.failed(CommonConstants.FAIL, "参数错误"); throw new ServiceException("业务异常"); ``` #### 3.3 分页响应 ```java // ❌ 旧代码 public ResultDomain> list(PageParam param) { Page page = workcaseMapper.selectPage(param); return ResultDomain.success(page.getRecords(), page.getTotal()); } // ✅ 新代码 public R> list(Page page, TbWorkcaseDTO query) { IPage result = workcaseMapper.selectPageVo(page, query); return R.ok(result); } ``` ### 步骤 4:租户隔离实现 #### 4.1 实体类添加租户字段 ```java @Data @TableName("tb_workcase") public class TbWorkcase { @TableId private Long id; private String title; // ✅ 新增租户字段 @TableField("tenant_id") private Long tenantId; // 其他字段... } ``` #### 4.2 Service 层自动注入租户 ```java @Service public class WorkcaseServiceImpl implements WorkcaseService { @Override public R save(TbWorkcaseDTO dto) { // ✅ 自动注入当前租户 PigxUser user = SecurityUtils.getUser(); dto.setTenantId(user.getTenantId()); dto.setCreateBy(user.getUsername()); dto.setCreateTime(LocalDateTime.now()); workcaseMapper.insert(dto); return R.ok(dto); } @Override public R> page(Page page, TbWorkcaseDTO query) { // ✅ 查询条件自动添加租户过滤 PigxUser user = SecurityUtils.getUser(); query.setTenantId(user.getTenantId()); return R.ok(workcaseMapper.selectPageVo(page, query)); } } ``` #### 4.3 Mapper 层租户隔离 ```xml ``` ### 步骤 5:配置 RemoteUserService #### 5.1 添加 Feign 客户端接口 ```java package com.pig4cloud.pigx.app.api.feign; import com.pig4cloud.pigx.common.core.constant.ServiceNameConstants; import com.pig4cloud.pigx.common.core.util.R; import com.pig4cloud.pigx.upms.api.entity.SysUser; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; @FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.UPMS_SERVICE) public interface RemoteUserService { /** * 根据用户ID查询用户信息 */ @GetMapping("/user/info/{id}") R selectById(@PathVariable("id") Long id); /** * 根据用户名查询用户信息 */ @GetMapping("/user/info") R selectByUsername(@RequestParam("username") String username); /** * 批量查询用户信息 */ @PostMapping("/user/list") R> selectBatchIds(@RequestBody List ids); } ``` #### 5.2 使用 RemoteUserService ```java @Service @RequiredArgsConstructor public class WorkcaseServiceImpl implements WorkcaseService { private final RemoteUserService remoteUserService; public TbWorkcaseDTO getWorkcaseDetail(Long id) { TbWorkcase workcase = workcaseMapper.selectById(id); TbWorkcaseDTO dto = BeanUtil.copyProperties(workcase, TbWorkcaseDTO.class); // 获取创建人信息 if (dto.getCreatorId() != null) { R creatorResult = remoteUserService.selectById(dto.getCreatorId()); if (creatorResult.isSuccess() && creatorResult.getData() != null) { dto.setCreatorName(creatorResult.getData().getUsername()); dto.setCreatorDeptName(creatorResult.getData().getDeptName()); } } // 获取处理人信息 if (dto.getAssigneeId() != null) { R assigneeResult = remoteUserService.selectById(dto.getAssigneeId()); if (assigneeResult.isSuccess() && assigneeResult.getData() != null) { dto.setAssigneeName(assigneeResult.getData().getUsername()); } } return dto; } } ``` ## 批量转换工具 ### 使用 IDE 批量替换(推荐) #### IntelliJ IDEA 1. **权限注解替换** - 查找:`@PreAuthorize\("hasAuthority\('([^:]+):([^:]+):([^']+)'\)"\)` - 替换:`@PreAuthorize("@pms.hasPermission('$1_$2_$3')")` - 选项:勾选 "Regex" 2. **响应格式替换** - 查找:`ResultDomain\.success\((.*?)\)` - 替换:`R.ok($1)` 3. **用户信息获取** - 查找:`JwtUtils\.getUserId\(\)` - 替换:`SecurityUtils.getUser().getId()` #### VS Code 使用 Find and Replace (Ctrl+Shift+H),启用正则表达式模式。 ### 使用脚本批量转换 创建 `convert-permissions.sh`: ```bash #!/bin/bash # 转换权限注解 find ./src -name "*.java" -type f -exec sed -i \ 's/@PreAuthorize("hasAuthority('\''\\([^:]*\\):\\([^:]*\\):\\([^'\'']*\\)'\''")"/@PreAuthorize("@pms.hasPermission('\''\\1_\\2_\\3'\'')"/g' {} \; # 转换响应格式 find ./src -name "*.java" -type f -exec sed -i \ 's/ResultDomain\.success(\(.*\))/R.ok(\1)/g' {} \; # 转换用户信息获取 find ./src -name "*.java" -type f -exec sed -i \ 's/JwtUtils\.getUserId()/SecurityUtils.getUser().getId()/g' {} \; echo "转换完成!" ``` ## 测试验证 ### 1. 单元测试示例 ```java @SpringBootTest @AutoConfigureMockMvc public class WorkcaseControllerTest { @Autowired private MockMvc mockMvc; @Test @WithMockUser(username = "admin", authorities = {"workcase_ticket_add"}) public void testCreateWorkcase() throws Exception { TbWorkcaseDTO dto = new TbWorkcaseDTO(); dto.setTitle("测试工单"); mockMvc.perform(post("/workcase") .contentType(MediaType.APPLICATION_JSON) .content(JSON.toJSONString(dto))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data.title").value("测试工单")); } @Test @WithMockUser(username = "user", authorities = {}) public void testCreateWorkcaseNoPermission() throws Exception { mockMvc.perform(post("/workcase") .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isForbidden()); } } ``` ### 2. 集成测试检查清单 - [ ] 权限注解正确转换 - [ ] 用户信息正确获取 - [ ] 租户数据正确隔离 - [ ] 响应格式符合规范 - [ ] RemoteUserService 调用成功 - [ ] 菜单权限正确配置 - [ ] 角色权限正确分配 ## 常见问题解决 ### Q1: @pms.hasPermission() 不生效 **原因**:没有正确配置 PermissionService Bean **解决**: ```java @Configuration public class SecurityConfig { @Bean("pms") public PermissionService permissionService() { return new PermissionService(); } } ``` ### Q2: RemoteUserService 调用失败 **原因**:Feign 客户端未正确配置 **解决**: 1. 检查 `@EnableFeignClients` 注解 2. 确认服务名称正确 3. 添加熔断处理 ```java @FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.UPMS_SERVICE, fallback = RemoteUserServiceFallback.class) ``` ### Q3: 租户数据泄露 **原因**:查询时未添加租户过滤 **解决**: 1. 使用 MyBatis-Plus 租户插件 2. 手动添加租户条件 ```java @Configuration public class MybatisPlusConfig { @Bean public TenantLineInnerInterceptor tenantLineInnerInterceptor() { return new TenantLineInnerInterceptor(new TenantLineHandler() { @Override public Expression getTenantId() { PigxUser user = SecurityUtils.getUser(); return new LongValue(user.getTenantId()); } @Override public String getTenantIdColumn() { return "tenant_id"; } }); } } ``` ## 迁移验证 完成转换后,执行以下验证: 1. **编译检查**:确保所有代码编译通过 2. **启动检查**:应用能正常启动 3. **权限测试**:各接口权限控制正确 4. **数据隔离**:租户数据正确隔离 5. **功能测试**:业务功能正常运行 ## 总结 权限迁移是整个系统迁移的核心部分,需要: 1. 仔细转换每个权限注解 2. 正确处理用户信息获取 3. 实现租户数据隔离 4. 充分测试验证 建议分模块逐步迁移,每完成一个模块就进行测试验证。