Files
urbanLifeline/.kiro/specs/urbanlifeline-to-pigx-migration/permission-conversion-guide.md
2026-01-14 15:42:26 +08:00

13 KiB
Raw Blame History

权限注解转换指南

目标

将 urbanLifeline 的权限体系完全迁移到 pigx 平台的权限模型。

核心概念对比

特性 urbanLifeline pigx
权限注解 @PreAuthorize("hasAuthority()") @PreAuthorize("@pms.hasPermission()")
权限格式 module:resource:action module_resource_action
用户获取 JwtUtils.getUserId() SecurityUtils.getUser()
用户服务 UserService (本地) RemoteUserService (Feign)
响应格式 ResultDomain<T> R<T>
租户支持 有 (tenant_id)

转换步骤详解

步骤 1权限注解转换

1.1 基本转换规则

// ❌ 旧代码 (urbanLifeline)
@PreAuthorize("hasAuthority('workcase:ticket:create')")
public ResultDomain<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO dto) {
    // ...
}

// ✅ 新代码 (pigx)
@PreAuthorize("@pms.hasPermission('workcase_ticket_add')")
public R<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO dto) {
    // ...
}

1.2 多权限组合

// ❌ 旧代码 - 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 动态权限检查

// ❌ 旧代码
@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 获取当前用户

// ❌ 旧代码
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<String> roles = user.getRoles(); // 角色列表

2.2 Service 层用户信息处理

// ❌ 旧代码
@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<SysUser> result = remoteUserService.selectById(assigneeId);
        if (result.isSuccess() && result.getData() != null) {
            SysUser assignee = result.getData();
            // 处理逻辑...
        } else {
            throw new BusinessException("用户不存在");
        }
    }
}

步骤 3响应格式转换

3.1 成功响应

// ❌ 旧代码
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 错误响应

// ❌ 旧代码
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 分页响应

// ❌ 旧代码
public ResultDomain<List<TbWorkcaseDTO>> list(PageParam param) {
    Page<TbWorkcase> page = workcaseMapper.selectPage(param);
    return ResultDomain.success(page.getRecords(), page.getTotal());
}

// ✅ 新代码
public R<IPage<TbWorkcaseDTO>> list(Page page, TbWorkcaseDTO query) {
    IPage<TbWorkcaseDTO> result = workcaseMapper.selectPageVo(page, query);
    return R.ok(result);
}

步骤 4租户隔离实现

4.1 实体类添加租户字段

@Data
@TableName("tb_workcase")
public class TbWorkcase {
    @TableId
    private Long id;

    private String title;

    // ✅ 新增租户字段
    @TableField("tenant_id")
    private Long tenantId;

    // 其他字段...
}

4.2 Service 层自动注入租户

@Service
public class WorkcaseServiceImpl implements WorkcaseService {

    @Override
    public R<TbWorkcaseDTO> 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<IPage<TbWorkcaseDTO>> page(Page page, TbWorkcaseDTO query) {
        // ✅ 查询条件自动添加租户过滤
        PigxUser user = SecurityUtils.getUser();
        query.setTenantId(user.getTenantId());

        return R.ok(workcaseMapper.selectPageVo(page, query));
    }
}

4.3 Mapper 层租户隔离

<!-- WorkcaseMapper.xml -->
<select id="selectPageVo" resultType="com.pig4cloud.pigx.app.api.dto.TbWorkcaseDTO">
    SELECT * FROM tb_workcase
    <where>
        <!-- ✅ 租户隔离条件 -->
        <if test="query.tenantId != null">
            AND tenant_id = #{query.tenantId}
        </if>
        <if test="query.title != null and query.title != ''">
            AND title LIKE CONCAT('%', #{query.title}, '%')
        </if>
    </where>
    ORDER BY create_time DESC
</select>

步骤 5配置 RemoteUserService

5.1 添加 Feign 客户端接口

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<SysUser> selectById(@PathVariable("id") Long id);

    /**
     * 根据用户名查询用户信息
     */
    @GetMapping("/user/info")
    R<SysUser> selectByUsername(@RequestParam("username") String username);

    /**
     * 批量查询用户信息
     */
    @PostMapping("/user/list")
    R<List<SysUser>> selectBatchIds(@RequestBody List<Long> ids);
}

5.2 使用 RemoteUserService

@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<SysUser> 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<SysUser> 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

#!/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. 单元测试示例

@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

解决

@Configuration
public class SecurityConfig {
    @Bean("pms")
    public PermissionService permissionService() {
        return new PermissionService();
    }
}

Q2: RemoteUserService 调用失败

原因Feign 客户端未正确配置

解决

  1. 检查 @EnableFeignClients 注解
  2. 确认服务名称正确
  3. 添加熔断处理
@FeignClient(contextId = "remoteUserService",
            value = ServiceNameConstants.UPMS_SERVICE,
            fallback = RemoteUserServiceFallback.class)

Q3: 租户数据泄露

原因:查询时未添加租户过滤

解决

  1. 使用 MyBatis-Plus 租户插件
  2. 手动添加租户条件
@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. 充分测试验证

建议分模块逐步迁移,每完成一个模块就进行测试验证。