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

490 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 权限注解转换指南
## 目标
将 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. 充分测试验证
建议分模块逐步迁移,每完成一个模块就进行测试验证。