# 权限注解转换指南

## 目标

将 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. 充分测试验证

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