Files
urbanLifeline/.kiro/specs/urbanlifeline-to-pigx-migration/permission-conversion-guide.md

490 lines
13 KiB
Markdown
Raw Normal View History

2026-01-14 15:42:26 +08:00
# 权限注解转换指南
## 目标
将 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 基本转换规则
```java
// ❌ 旧代码 (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 多权限组合
```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<String> 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<SysUser> 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<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 实体类添加租户字段
```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<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 层租户隔离
```xml
<!-- 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 客户端接口
```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<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
```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<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`
```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. 充分测试验证
建议分模块逐步迁移,每完成一个模块就进行测试验证。