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

391 lines
11 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.

# 权限注解转换指南
## 概述
本指南详细说明了如何将 urbanLifelineServ 的权限注解迁移到 pigx 平台的权限体系。
## 核心变更
### 1. 权限注解格式
| 特性 | urbanLifelineServ | pigx |
|------|------------------|------|
| 注解类 | @PreAuthorize | @PreAuthorize |
| 权限判断方法 | hasAuthority() | @pms.hasPermission() |
| 权限标识格式 | module:resource:action | module_resource_action |
| 分隔符 | 冒号 `:` | 下划线 `_` |
### 2. 用户信息获取
| 功能 | urbanLifelineServ | pigx |
|------|------------------|------|
| 获取用户ID | JwtUtils.getUserId() | SecurityUtils.getUser().getId() |
| 获取用户名 | JwtUtils.getUsername() | SecurityUtils.getUser().getUsername() |
| 获取租户ID | 不支持 | SecurityUtils.getUser().getTenantId() |
| 获取部门ID | 不支持 | SecurityUtils.getUser().getDeptId() |
### 3. 响应格式
| 特性 | urbanLifelineServ | pigx |
|------|------------------|------|
| 响应类 | ResultDomain<T> | R<T> |
| 成功响应 | ResultDomain.success(data) | R.ok(data) |
| 失败响应 | ResultDomain.fail(msg) | R.failed(msg) |
| 列表字段 | dataList | data |
## 转换步骤
### 步骤1权限注解转换
#### 1.1 基本转换
```java
// 转换前
@PreAuthorize("hasAuthority('workcase:ticket:create')")
public ResultDomain<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
return workcaseService.createWorkcase(workcase);
}
// 转换后
@PreAuthorize("@pms.hasPermission('workcase_ticket_add')")
public R<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
return R.ok(workcaseService.createWorkcase(workcase));
}
```
#### 1.2 多权限判断
```java
// 转换前
@PreAuthorize("hasAuthority('workcase:ticket:view') or hasAuthority('workcase:ticket:admin')")
public ResultDomain<List<TbWorkcaseDTO>> listWorkcase() {
return ResultDomain.success(workcaseService.list());
}
// 转换后
@PreAuthorize("@pms.hasPermission('workcase_ticket_view') or @pms.hasPermission('workcase_ticket_admin')")
public R<List<TbWorkcaseDTO>> listWorkcase() {
return R.ok(workcaseService.list());
}
```
### 步骤2用户信息获取转换
#### 2.1 Service层用户信息获取
```java
// 转换前
@Service
public class WorkcaseServiceImpl implements WorkcaseService {
public TbWorkcaseDTO createWorkcase(TbWorkcaseDTO workcase) {
Long userId = JwtUtils.getUserId();
String username = JwtUtils.getUsername();
workcase.setCreateBy(userId);
workcase.setCreateByName(username);
workcase.setCreateTime(new Date());
return workcaseMapper.insert(workcase);
}
}
// 转换后
@Service
public class WorkcaseServiceImpl implements WorkcaseService {
public TbWorkcaseDTO createWorkcase(TbWorkcaseDTO workcase) {
PigxUser user = SecurityUtils.getUser();
workcase.setCreateBy(user.getId());
workcase.setCreateByName(user.getUsername());
workcase.setTenantId(user.getTenantId()); // 新增:租户隔离
workcase.setDeptId(user.getDeptId()); // 新增:部门信息
workcase.setCreateTime(LocalDateTime.now());
return workcaseMapper.insert(workcase);
}
}
```
#### 2.2 远程用户服务调用
```java
// 转换前
@Service
public class WorkcaseServiceImpl {
@Autowired
private UserService userService;
public void assignWorkcase(String workcaseId, Long assigneeId) {
User assignee = userService.getById(assigneeId);
if (assignee == null) {
throw new BusinessException("用户不存在");
}
// 处理逻辑...
}
}
// 转换后
@Service
public class WorkcaseServiceImpl {
@Autowired
private RemoteUserService remoteUserService;
public void assignWorkcase(String workcaseId, Long assigneeId) {
R<SysUser> result = remoteUserService.selectById(assigneeId);
if (!result.isSuccess() || result.getData() == null) {
throw new BusinessException("用户不存在");
}
SysUser assignee = result.getData();
// 处理逻辑...
}
}
```
### 步骤3响应格式转换
#### 3.1 Controller响应转换
```java
// 转换前
@RestController
@RequestMapping("/api/workcase")
public class WorkcaseController {
// 单个对象返回
@GetMapping("/{id}")
public ResultDomain<TbWorkcaseDTO> getById(@PathVariable String id) {
TbWorkcaseDTO workcase = workcaseService.getById(id);
if (workcase == null) {
return ResultDomain.fail("工单不存在");
}
return ResultDomain.success(workcase);
}
// 列表返回
@GetMapping("/list")
public ResultDomain<List<TbWorkcaseDTO>> list() {
List<TbWorkcaseDTO> list = workcaseService.list();
ResultDomain<List<TbWorkcaseDTO>> result = ResultDomain.success();
result.setDataList(list); // 注意使用dataList字段
return result;
}
}
// 转换后
@RestController
@RequestMapping("/workcase")
public class WorkcaseController {
// 单个对象返回
@GetMapping("/{id}")
public R<TbWorkcaseDTO> getById(@PathVariable String id) {
TbWorkcaseDTO workcase = workcaseService.getById(id);
if (workcase == null) {
return R.failed("工单不存在");
}
return R.ok(workcase);
}
// 列表返回
@GetMapping("/list")
public R<List<TbWorkcaseDTO>> list() {
List<TbWorkcaseDTO> list = workcaseService.list();
return R.ok(list); // 直接返回列表不使用dataList
}
// 分页返回
@GetMapping("/page")
public R<IPage<TbWorkcaseDTO>> page(Page page) {
return R.ok(workcaseService.page(page));
}
}
```
### 步骤4数据权限适配
#### 4.1 添加租户隔离
```java
// 转换前 - 无租户隔离
@Service
public class WorkcaseServiceImpl {
public List<TbWorkcaseDTO> listMyWorkcase() {
Long userId = JwtUtils.getUserId();
return workcaseMapper.selectByUserId(userId);
}
}
// 转换后 - 支持租户隔离
@Service
public class WorkcaseServiceImpl {
public List<TbWorkcaseDTO> listMyWorkcase() {
PigxUser user = SecurityUtils.getUser();
QueryWrapper<TbWorkcaseDTO> wrapper = new QueryWrapper<>();
wrapper.eq("tenant_id", user.getTenantId()) // 租户隔离
.eq("create_by", user.getId());
return workcaseMapper.selectList(wrapper);
}
}
```
## 批量转换工具
### 使用IDE批量替换
#### IntelliJ IDEA 正则替换
1. **查找模式** (启用正则表达式):
```regex
@PreAuthorize\("hasAuthority\('([^:]+):([^:]+):([^']+)'\)"\)
```
2. **替换为**:
```regex
@PreAuthorize("@pms.hasPermission('$1_$2_$3')")
```
3. **动作映射** (执行第二次替换):
- 查找: `_create'` 替换为: `_add'`
- 查找: `_update'` 替换为: `_edit'`
- 查找: `_delete'` 替换为: `_del'`
### 命令行批量转换脚本
```bash
#!/bin/bash
# convert-permissions.sh
# 查找所有Java文件并转换权限注解
find . -name "*.java" -type f -exec sed -i.bak \
-e "s/@PreAuthorize(\"hasAuthority('\([^:]*\):\([^:]*\):\([^']*\)')\")/@PreAuthorize(\"@pms.hasPermission('\1_\2_\3')\")/g" \
-e "s/_create')/_add')/g" \
-e "s/_update')/_edit')/g" \
-e "s/_delete')/_del')/g" {} \;
# 转换JwtUtils为SecurityUtils
find . -name "*.java" -type f -exec sed -i.bak \
-e "s/JwtUtils.getUserId()/SecurityUtils.getUser().getId()/g" \
-e "s/JwtUtils.getUsername()/SecurityUtils.getUser().getUsername()/g" {} \;
# 转换ResultDomain为R
find . -name "*.java" -type f -exec sed -i.bak \
-e "s/ResultDomain.success(/R.ok(/g" \
-e "s/ResultDomain.fail(/R.failed(/g" \
-e "s/ResultDomain</R</g" {} \;
```
## 需要手工处理的情况
### 1. 复杂的权限逻辑
```java
// 需要手工审查的复杂权限
@PreAuthorize("hasAuthority('workcase:ticket:view') and #workcase.createBy == authentication.principal.userId")
public ResultDomain<TbWorkcaseDTO> getMyWorkcase(@PathVariable String id, TbWorkcaseDTO workcase) {
// 这种情况需要根据pigx的数据权限机制重新设计
}
```
### 2. 自定义权限判断
```java
// 转换前
if (SecurityContextHolder.getContext().getAuthentication().getAuthorities()
.contains(new SimpleGrantedAuthority("workcase:ticket:admin"))) {
// 管理员逻辑
}
// 转换后
if (SecurityUtils.getUser().getAuthorities()
.contains("workcase_ticket_admin")) {
// 管理员逻辑
}
```
### 3. 异步任务中的用户信息
```java
// 转换前
@Async
public void processAsync() {
Long userId = JwtUtils.getUserId(); // 异步线程中可能获取不到
}
// 转换后
@Async
public void processAsync() {
// 需要在调用异步方法前获取用户信息并传递
PigxUser user = SecurityUtils.getUser();
processAsyncWithUser(user);
}
```
## 测试验证清单
### 权限测试
- [ ] 所有 @PreAuthorize 注解已转换为 @pms.hasPermission 格式
- [ ] 权限标识符已从冒号改为下划线
- [ ] 动作已正确映射 (create→add, update→edit, delete→del)
- [ ] 多权限判断逻辑正确
### 用户信息测试
- [ ] SecurityUtils.getUser() 能正确获取用户信息
- [ ] 租户ID正确设置到业务数据
- [ ] RemoteUserService 调用正常
### 响应格式测试
- [ ] 所有接口返回 R<T> 格式
- [ ] 前端能正确解析新的响应格式
- [ ] 错误信息正确传递
### 数据权限测试
- [ ] 租户数据隔离正常
- [ ] 部门数据权限正常
- [ ] 个人数据权限正常
## 常见问题
### Q1: @pms.hasPermission 中的 @pms 是什么?
A: `@pms` 是 pigx 权限管理系统的 SpEL 表达式前缀,用于调用权限判断方法。这是 pigx 框架的固定写法,必须保留。
### Q2: 为什么要将 create 改为 add
A: 这是 pigx 平台的命名规范,保持统一的动作命名有助于权限管理的标准化。常见映射:
- create → add (新增)
- update → edit (编辑)
- delete → del (删除)
- view → view (查看)
### Q3: 如何处理没有对应 pigx 用户的情况?
A: 所有业务用户必须在 pigx 的 sys_user 表中存在。如果是数据迁移,需要先创建对应的 pigx 用户,或建立用户映射关系。
### Q4: 租户ID是必须的吗
A: 是的。pigx 是多租户系统,所有业务表都需要 tenant_id 字段。即使是单租户使用也需要设置默认租户ID通常为1
### Q5: 如何调试权限问题?
A: 可以通过以下方式调试:
1. 查看 pigx 日志中的权限判断记录
2. 使用 SecurityUtils.getUser().getAuthorities() 查看当前用户权限
3. 检查 sys_menu 表中的权限配置
4. 验证 sys_role_menu 表中的角色权限关联
## 相关文档
- [权限标识映射表](./permission-mapping.md)
- [数据库迁移指南](./database-migration.md)
- [前端适配指南](./frontend-migration.md)