ether-docs/_archive/domains-old/permission-upgrade-v3/spec.md

47 KiB
Raw Permalink Blame History

Ether 权限体系升级实施计划

状态: 已完成 - 2026-02-27

所有15个任务已完成编译验证通过单元测试通过。

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 升级 Ether 权限系统实现13个角色体系、按钮级权限控制、状态驱动权限覆盖三端管理后台、员工端、业主端确保不影响现有业务功能。

Architecture: 采用渐进式升级策略,先扩展后端角色和权限定义,再升级前端权限检查机制,最后进行数据迁移和测试验证。核心原则是向后兼容,所有新增角色和权限不影响现有功能。

Tech Stack:

  • 后端: Java 17, Spring Boot 3.x, JPA, PostgreSQL
  • 前端: Vue 3, TypeScript, Pinia, Ant Design Vue

一、当前状态分析

1.1 现有角色体系6个角色

角色编码 角色名称 数据范围
SUPER_ADMIN 超级管理员 ALL
PROJECT_MANAGER 项目经理 ALL
PROPERTY_MANAGER 物业经理 ALL
CUSTOMER_SERVICE 客服人员 DEPARTMENT
MAINTENANCE_STAFF 维修人员 SELF
SECURITY_STAFF 保安人员 SELF
RECEPTIONIST 前台接待 SELF

1.2 目标角色体系13个角色

层级 角色编码 角色名称 数据范围 终端类型
系统管理层 SUPER_ADMIN 超级管理员 ALL 全终端
系统管理层 SYS_ADMIN 系统管理员 ALL 管理后台
项目管理层 PROPERTY_MANAGER 物业经理 PROJECT 全终端
项目管理层 PROJECT_MANAGER 项目经理 PROJECT 全终端
部门主管层 ENGINEERING_LEAD 工程主管 DEPARTMENT 全终端
部门主管层 SECURITY_LEAD 安保主管 DEPARTMENT 全终端
部门主管层 CLEANING_LEAD 保洁主管 DEPARTMENT 全终端
部门主管层 FINANCE_LEAD 财务主管 DEPARTMENT 全终端
一线执行层 MAINTENANCE_STAFF 维修人员 SELF 移动端优先
一线执行层 SECURITY_STAFF 安保人员 SELF 移动端优先
一线执行层 CLEANING_STAFF 保洁人员 SELF 移动端优先
服务支持层 CS_STAFF 客服人员 PROJECT 全终端
外部用户层 OWNER 业主 SELF 业主端APP

1.3 数据范围扩展

当前 目标 说明
ALL ALL 全部数据
- PROJECT 本项目数据(新增)
DEPARTMENT DEPARTMENT 本部门数据
SELF SELF 仅本人数据

1.4 关键文件清单

后端文件:

  • ether-auth/src/main/java/com/ether/auth/entity/Role.java - 角色实体
  • ether-auth/src/main/java/com/ether/auth/entity/Permission.java - 权限实体
  • ether-auth/src/main/java/com/ether/auth/constant/RoleTemplate.java - 角色模板
  • ether-auth/src/main/java/com/ether/auth/service/PermissionService.java - 权限服务
  • ether-auth/src/main/java/com/ether/auth/config/DataInitializer.java - 数据初始化
  • ether-common/ether-common-core/src/main/java/com/ether/common/util/DataScopeHelper.java - 数据权限工具

前端文件:

  • ether-ui-admin/src/stores/permission.ts - 权限Store
  • ether-ui-admin/src/directives/permission.ts - 权限指令
  • ether-ui-admin/src/router/guard/permissionGuard.ts - 路由守卫
  • ether-app-employee/src/stores/user.ts - 员工端用户Store
  • ether-app-owner/src/stores/user.ts - 业主端用户Store

二、升级策略

2.1 核心原则

  1. 向后兼容: 所有新增角色和权限不影响现有功能
  2. 渐进式升级: 分阶段实施,每阶段可独立验证
  3. 数据安全: 提供数据迁移脚本和回滚脚本
  4. 最小改动: 尽量复用现有代码,减少重构范围

2.2 升级阶段

阶段1: 后端基础升级(不影响现有功能)
├── 扩展数据范围枚举
├── 扩展角色模板
├── 新增按钮级权限编码
└── 新增状态驱动权限配置

阶段2: 前端权限增强
├── 管理后台权限指令增强
├── 员工端权限Store新增
└── 业主端权限检查增强

阶段3: 数据迁移
├── 新角色初始化
├── 新权限初始化
└── 角色权限关联

阶段4: 测试验证
├── 单元测试
├── 集成测试
└── E2E测试

三、详细任务清单

Task 1: 后端数据范围枚举扩展

Files:

  • Modify: ether-auth/src/main/java/com/ether/auth/entity/Role.java
  • Modify: ether-common/ether-common-core/src/main/java/com/ether/common/util/DataScopeHelper.java
  • Modify: ether-common/ether-common-core/src/main/java/com/ether/common/context/DataScopeContext.java

Step 1: 扩展 DataScope 枚举

Role.java 中添加 PROJECT 枚举值:

public enum DataScope {
    ALL,        // 全部数据
    PROJECT,    // 本项目数据(新增)
    DEPARTMENT, // 本部门数据
    SELF        // 仅本人数据
}

Step 2: 更新 DataScopeHelper

DataScopeHelper.javacanAccess 方法中添加 PROJECT 分支:

public static boolean canAccess(UUID dataProjectId, UUID dataDeptId, UUID dataCreatorId) {
    DataScopeInfo scope = DataScopeContext.getDataScopeInfo();
    
    return switch (scope.getScope()) {
        case ALL -> true;
        case PROJECT -> dataProjectId.equals(scope.getProjectId());
        case DEPARTMENT -> dataProjectId.equals(scope.getProjectId()) && 
                          (dataDeptId == null || dataDeptId.equals(scope.getDepartmentId()));
        case SELF -> dataCreatorId.equals(scope.getUserId());
    };
}

Step 3: 更新 DataScopeContext

DataScopeInfo 类中添加 projectId 字段:

public static class DataScopeInfo {
    private final String dataScope;
    private final UUID userId;
    private final UUID projectId;      // 新增
    private final UUID departmentId;
}

Step 4: 编译验证

cd ether-auth && mvn compile -q
cd ether-common/ether-common-core && mvn compile -q

Step 5: Commit

git add ether-auth/src/main/java/com/ether/auth/entity/Role.java
git add ether-common/ether-common-core/src/main/java/com/ether/common/util/DataScopeHelper.java
git add ether-common/ether-common-core/src/main/java/com/ether/common/context/DataScopeContext.java
git commit -m "feat(auth): 扩展数据范围枚举新增PROJECT级别"

Task 2: 后端角色模板扩展

Files:

  • Modify: ether-auth/src/main/java/com/ether/auth/constant/RoleTemplate.java

Step 1: 更新角色模板

添加新的角色模板定义:

public class RoleTemplate {
    
    public static final List<RoleTemplateDefinition> PROJECT_ROLES = List.of(
        // 项目管理层
        new RoleTemplateDefinition("PROPERTY_MANAGER", "物业经理", 
            RoleType.SYSTEM, DataScope.PROJECT, 1, "多项目运营管理"),
        new RoleTemplateDefinition("PROJECT_MANAGER", "项目经理", 
            RoleType.SYSTEM, DataScope.PROJECT, 2, "单项目管理"),
        
        // 部门主管层
        new RoleTemplateDefinition("ENGINEERING_LEAD", "工程主管", 
            RoleType.SYSTEM, DataScope.DEPARTMENT, 3, "工程部管理"),
        new RoleTemplateDefinition("SECURITY_LEAD", "安保主管", 
            RoleType.SYSTEM, DataScope.DEPARTMENT, 4, "安保部管理"),
        new RoleTemplateDefinition("CLEANING_LEAD", "保洁主管", 
            RoleType.SYSTEM, DataScope.DEPARTMENT, 5, "保洁部管理"),
        new RoleTemplateDefinition("FINANCE_LEAD", "财务主管", 
            RoleType.SYSTEM, DataScope.DEPARTMENT, 6, "财务部管理"),
        
        // 一线执行层
        new RoleTemplateDefinition("MAINTENANCE_STAFF", "维修人员", 
            RoleType.SYSTEM, DataScope.SELF, 7, "工单接单、维修执行"),
        new RoleTemplateDefinition("SECURITY_STAFF", "安保人员", 
            RoleType.SYSTEM, DataScope.SELF, 8, "安保巡检、访客核验"),
        new RoleTemplateDefinition("CLEANING_STAFF", "保洁人员", 
            RoleType.SYSTEM, DataScope.SELF, 9, "保洁执行、品质检查"),
        
        // 服务支持层
        new RoleTemplateDefinition("CS_STAFF", "客服人员", 
            RoleType.SYSTEM, DataScope.PROJECT, 10, "业主服务、访客核验")
    );
    
    public record RoleTemplateDefinition(
        String roleCode,
        String roleName,
        RoleType roleType,
        DataScope dataScope,
        int sortOrder,
        String description
    ) {}
}

Step 2: 编译验证

cd ether-auth && mvn compile -q

Step 3: Commit

git add ether-auth/src/main/java/com/ether/auth/constant/RoleTemplate.java
git commit -m "feat(auth): 扩展角色模板新增7个角色定义"

Task 3: 后端按钮级权限编码定义

Files:

  • Create: ether-auth/src/main/java/com/ether/auth/constant/PermissionTemplate.java

Step 1: 创建权限模板类

package com.ether.auth.constant;

import java.util.List;

public class PermissionTemplate {
    
    public static final List<PermissionDefinition> BUTTON_PERMISSIONS = List.of(
        // 工单操作权限
        new PermissionDefinition("ops", "work_order", "view", "查看工单", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "create", "创建工单", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "assign", "分配工单", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "accept", "接单", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "start", "开始处理", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "complete", "完成工单", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "transfer", "转单", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "close", "关闭工单", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "report_fee", "填报费用", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "audit_fee", "费用审核", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "audit_quality", "质量审核", "BUTTON"),
        new PermissionDefinition("ops", "work_order", "delete", "删除工单", "BUTTON"),
        
        // 巡检操作权限
        new PermissionDefinition("ops", "inspection", "view", "查看巡检", "BUTTON"),
        new PermissionDefinition("ops", "inspection", "start", "开始巡检", "BUTTON"),
        new PermissionDefinition("ops", "inspection", "scan", "扫码签到", "BUTTON"),
        new PermissionDefinition("ops", "inspection", "report", "异常上报", "BUTTON"),
        new PermissionDefinition("ops", "inspection", "complete", "完成巡检", "BUTTON"),
        new PermissionDefinition("ops", "inspection", "plan", "制定计划", "BUTTON"),
        new PermissionDefinition("ops", "inspection", "assign", "指派任务", "BUTTON"),
        new PermissionDefinition("ops", "inspection", "force_close", "强制闭环", "BUTTON"),
        
        // 设备操作权限
        new PermissionDefinition("mdm", "equipment", "view", "查看设备", "BUTTON"),
        new PermissionDefinition("mdm", "equipment", "scan", "扫码巡检", "BUTTON"),
        new PermissionDefinition("mdm", "equipment", "maintain", "维护记录", "BUTTON"),
        new PermissionDefinition("mdm", "equipment", "edit", "编辑设备", "BUTTON"),
        new PermissionDefinition("mdm", "equipment", "create", "新增设备", "BUTTON"),
        new PermissionDefinition("mdm", "equipment", "delete", "删除设备", "BUTTON"),
        
        // 访客操作权限
        new PermissionDefinition("ops", "visitor", "view", "查看访客", "BUTTON"),
        new PermissionDefinition("ops", "visitor", "register", "访客登记", "BUTTON"),
        new PermissionDefinition("ops", "visitor", "verify", "访客核验", "BUTTON"),
        new PermissionDefinition("ops", "visitor", "release", "访客放行", "BUTTON"),
        new PermissionDefinition("ops", "visitor", "export", "导出记录", "BUTTON"),
        
        // 财务操作权限
        new PermissionDefinition("finance", "bill", "view", "查看账单", "BUTTON"),
        new PermissionDefinition("finance", "bill", "create", "生成账单", "BUTTON"),
        new PermissionDefinition("finance", "bill", "adjust", "调整账单", "BUTTON"),
        new PermissionDefinition("finance", "bill", "collect", "收费登记", "BUTTON"),
        new PermissionDefinition("finance", "bill", "pay", "在线缴费", "BUTTON"),
        new PermissionDefinition("finance", "bill", "audit", "收费审核", "BUTTON"),
        new PermissionDefinition("finance", "bill", "reduce", "减免审批", "BUTTON"),
        new PermissionDefinition("finance", "bill", "export", "导出报表", "BUTTON")
    );
    
    public record PermissionDefinition(
        String module,
        String resource,
        String action,
        String name,
        String type
    ) {
        public String code() {
            return module + ":" + resource + ":" + action;
        }
    }
}

Step 2: 编译验证

cd ether-auth && mvn compile -q

Step 3: Commit

git add ether-auth/src/main/java/com/ether/auth/constant/PermissionTemplate.java
git commit -m "feat(auth): 新增按钮级权限模板定义"

Task 4: 后端状态驱动权限配置

Files:

  • Create: ether-ops/src/main/java/com/ether/ops/constants/WorkOrderAction.java
  • Modify: ether-ops/src/main/java/com/ether/ops/constants/WorkOrderStatus.java

Step 1: 创建工单操作枚举

package com.ether.ops.constants;

public enum WorkOrderAction {
    VIEW, CREATE, EDIT, DELETE,
    ASSIGN, ACCEPT, START, COMPLETE, TRANSFER, CLOSE, SUSPEND, RESUME, RETURN,
    EVALUATE, REPORT_FEE, AUDIT_FEE, AUDIT_QUALITY
}

Step 2: 更新工单状态枚举

WorkOrderStatus.java 中添加状态-操作映射:

public enum WorkOrderStatus {
    CREATED("已创建"),
    ASSIGNED("已分配"),
    ACCEPTED("已接单"),
    IN_PROGRESS("处理中"),
    PENDING("待确认"),
    COMPLETED("已完成"),
    CLOSED("已关闭"),
    SUSPENDED("已挂起"),
    RETURNED("已退回");
    
    private final String description;
    
    WorkOrderStatus(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
    
    public Set<WorkOrderAction> getAllowedActions() {
        return switch (this) {
            case CREATED -> Set.of(WorkOrderAction.ASSIGN, WorkOrderAction.EDIT, WorkOrderAction.DELETE);
            case ASSIGNED -> Set.of(WorkOrderAction.ACCEPT, WorkOrderAction.TRANSFER, WorkOrderAction.SUSPEND, WorkOrderAction.RETURN);
            case ACCEPTED -> Set.of(WorkOrderAction.START, WorkOrderAction.TRANSFER, WorkOrderAction.SUSPEND);
            case IN_PROGRESS -> Set.of(WorkOrderAction.COMPLETE, WorkOrderAction.REPORT_FEE, WorkOrderAction.SUSPEND);
            case PENDING -> Set.of(WorkOrderAction.COMPLETE, WorkOrderAction.AUDIT_QUALITY);
            case COMPLETED -> Set.of(WorkOrderAction.EVALUATE, WorkOrderAction.CLOSE, WorkOrderAction.AUDIT_FEE);
            case CLOSED -> Set.of();
            case SUSPENDED -> Set.of(WorkOrderAction.RESUME);
            case RETURNED -> Set.of(WorkOrderAction.ASSIGN, WorkOrderAction.DELETE);
        };
    }
    
    public boolean isActionAllowed(WorkOrderAction action) {
        return getAllowedActions().contains(action);
    }
}

Step 3: 编译验证

cd ether-ops && mvn compile -q

Step 4: Commit

git add ether-ops/src/main/java/com/ether/ops/constants/WorkOrderAction.java
git add ether-ops/src/main/java/com/ether/ops/constants/WorkOrderStatus.java
git commit -m "feat(ops): 新增工单状态驱动权限配置"

Task 5: 后端权限服务增强

Files:

  • Modify: ether-auth/src/main/java/com/ether/auth/service/PermissionService.java

Step 1: 添加状态驱动权限检查方法

/**
 * 检查用户是否有权限在指定状态下执行操作
 */
public boolean hasActionPermission(UUID userId, UUID projectId, String permissionCode, 
                                    String entityType, String status, String action) {
    // 1. 检查功能权限
    if (!hasPermission(userId, projectId, permissionCode)) {
        return false;
    }
    
    // 2. 检查状态是否允许该操作
    if (entityType.equals("work_order")) {
        try {
            WorkOrderStatus workOrderStatus = WorkOrderStatus.valueOf(status);
            WorkOrderAction workOrderAction = WorkOrderAction.valueOf(action);
            if (!workOrderStatus.isActionAllowed(workOrderAction)) {
                return false;
            }
        } catch (IllegalArgumentException e) {
            log.warn("Invalid status or action: status={}, action={}", status, action);
            return false;
        }
    }
    
    return true;
}

/**
 * 获取用户在指定状态下可执行的操作列表
 */
public Set<String> getAllowedActions(UUID userId, UUID projectId, String entityType, String status) {
    Set<String> allowedActions = new HashSet<>();
    
    if (entityType.equals("work_order")) {
        try {
            WorkOrderStatus workOrderStatus = WorkOrderStatus.valueOf(status);
            Set<WorkOrderAction> statusActions = workOrderStatus.getAllowedActions();
            
            for (WorkOrderAction action : statusActions) {
                String permissionCode = "ops:work_order:" + action.name().toLowerCase();
                if (hasPermission(userId, projectId, permissionCode)) {
                    allowedActions.add(action.name());
                }
            }
        } catch (IllegalArgumentException e) {
            log.warn("Invalid status: status={}", status);
        }
    }
    
    return allowedActions;
}

Step 2: 编译验证

cd ether-auth && mvn compile -q

Step 3: Commit

git add ether-auth/src/main/java/com/ether/auth/service/PermissionService.java
git commit -m "feat(auth): 新增状态驱动权限检查方法"

Task 6: 后端API接口扩展

Files:

  • Modify: ether-auth/src/main/java/com/ether/auth/controller/PermissionController.java

Step 1: 添加新API接口

/**
 * 检查状态驱动权限
 */
@PostMapping("/check-action")
public Result<Boolean> checkActionPermission(
    @RequestParam String permissionCode,
    @RequestParam String entityType,
    @RequestParam String status,
    @RequestParam String action
) {
    UUID userId = getCurrentUserId();
    UUID projectId = getCurrentProjectId();
    
    boolean hasPermission = permissionService.hasActionPermission(
        userId, projectId, permissionCode, entityType, status, action
    );
    
    return Result.success(hasPermission);
}

/**
 * 获取可执行操作列表
 */
@GetMapping("/allowed-actions")
public Result<Set<String>> getAllowedActions(
    @RequestParam String entityType,
    @RequestParam String status
) {
    UUID userId = getCurrentUserId();
    UUID projectId = getCurrentProjectId();
    
    Set<String> actions = permissionService.getAllowedActions(
        userId, projectId, entityType, status
    );
    
    return Result.success(actions);
}

Step 2: 编译验证

cd ether-auth && mvn compile -q

Step 3: Commit

git add ether-auth/src/main/java/com/ether/auth/controller/PermissionController.java
git commit -m "feat(auth): 新增状态驱动权限API接口"

Task 7: 前端管理后台权限Store增强

Files:

  • Modify: ether-ui-admin/src/stores/permission.ts

Step 1: 添加状态驱动权限检查方法

// 在 permission store 中添加

interface ActionPermissionConfig {
  permission: string
  status: string
  action: string
  entityType?: string
}

const checkActionPermission = async (config: ActionPermissionConfig): Promise<boolean> => {
  try {
    const response = await request.post('/api/v1/auth/permissions/check-action', null, {
      params: {
        permissionCode: config.permission,
        entityType: config.entityType || 'work_order',
        status: config.status,
        action: config.action
      }
    })
    return response.data === true
  } catch (error) {
    console.error('Failed to check action permission:', error)
    return false
  }
}

const getAllowedActions = async (entityType: string, status: string): Promise<string[]> => {
  try {
    const response = await request.get('/api/v1/auth/permissions/allowed-actions', {
      params: { entityType, status }
    })
    return response.data || []
  } catch (error) {
    console.error('Failed to get allowed actions:', error)
    return []
  }
}

return {
  // ... 现有导出
  checkActionPermission,
  getAllowedActions
}

Step 2: 编译验证

cd ether-ui-admin && npx vue-tsc --noEmit

Step 3: Commit

git add ether-ui-admin/src/stores/permission.ts
git commit -m "feat(ui-admin): 新增状态驱动权限检查方法"

Task 8: 前端管理后台权限指令增强

Files:

  • Modify: ether-ui-admin/src/directives/permission.ts

Step 1: 增强权限指令支持状态检查

import type { Directive, DirectiveBinding } from 'vue'
import { usePermissionStore } from '@/stores/permission'

interface PermissionConfig {
  permission?: string | string[]
  status?: string
  action?: string
  entityType?: string
  mode?: 'remove' | 'disable'
}

function checkPermission(value: string | string[] | PermissionConfig): boolean {
  const permissionStore = usePermissionStore()
  
  // 简单权限检查
  if (typeof value === 'string') {
    return permissionStore.hasPermissionCode(value)
  }
  
  // 数组权限检查
  if (Array.isArray(value)) {
    return permissionStore.hasAnyPermission(value)
  }
  
  // 复杂权限配置检查
  const config = value as PermissionConfig
  
  // 1. 检查功能权限
  if (config.permission) {
    const permissions = Array.isArray(config.permission) 
      ? config.permission 
      : [config.permission]
    if (!permissionStore.hasAnyPermission(permissions)) {
      return false
    }
  }
  
  // 2. 检查状态驱动权限(异步,需要在组件中处理)
  // 这里只做同步检查,状态检查需要在组件中使用 checkActionPermission
  
  return true
}

export const vPermission: Directive<HTMLElement, string | string[] | PermissionConfig> = {
  mounted(el: HTMLElement, binding: DirectiveBinding<string | string[] | PermissionConfig>) {
    const config = binding.value
    const mode = typeof config === 'object' && 'mode' in config ? config.mode : 'remove'
    
    if (!checkPermission(config)) {
      if (mode === 'disable') {
        el.setAttribute('disabled', 'true')
        el.classList.add('is-disabled')
        el.style.pointerEvents = 'none'
        el.style.opacity = '0.5'
      } else {
        el.parentNode?.removeChild(el)
      }
    }
  }
}

// 导出类型
export type { PermissionConfig }

Step 2: 编译验证

cd ether-ui-admin && npx vue-tsc --noEmit

Step 3: Commit

git add ether-ui-admin/src/directives/permission.ts
git commit -m "feat(ui-admin): 增强权限指令支持状态检查配置"

Task 9: 前端员工端权限Store新增

Files:

  • Create: ether-app-employee/src/stores/permission.ts

Step 1: 创建员工端权限Store

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { request } from '@/utils/request'

export interface PermissionInfo {
  code: string
  name: string
  type: string
}

export const usePermissionStore = defineStore('employee-permission', () => {
  const permissions = ref<PermissionInfo[]>([])
  const isInitialized = ref(false)
  const loading = ref(false)

  const hasPermissionCode = (code: string): boolean => {
    return permissions.value.some(p => p.code === code)
  }

  const hasAnyPermission = (codes: string[]): boolean => {
    return codes.some(code => hasPermissionCode(code))
  }

  const fetchPermissions = async () => {
    if (isInitialized.value) return
    
    loading.value = true
    try {
      const response = await request.get('/api/v1/auth/permissions/my-permissions')
      permissions.value = response.data || []
      isInitialized.value = true
    } catch (error) {
      console.error('Failed to fetch permissions:', error)
    } finally {
      loading.value = false
    }
  }

  const checkActionPermission = async (
    permissionCode: string,
    entityType: string,
    status: string,
    action: string
  ): Promise<boolean> => {
    try {
      const response = await request.post('/api/v1/auth/permissions/check-action', null, {
        params: { permissionCode, entityType, status, action }
      })
      return response.data === true
    } catch (error) {
      console.error('Failed to check action permission:', error)
      return false
    }
  }

  const clearPermissions = () => {
    permissions.value = []
    isInitialized.value = false
  }

  return {
    permissions,
    isInitialized,
    loading,
    hasPermissionCode,
    hasAnyPermission,
    fetchPermissions,
    checkActionPermission,
    clearPermissions
  }
})

Step 2: 编译验证

cd ether-app-employee && npx vue-tsc --noEmit

Step 3: Commit

git add ether-app-employee/src/stores/permission.ts
git commit -m "feat(app-employee): 新增权限Store"

Task 10: 前端员工端权限指令新增

Files:

  • Create: ether-app-employee/src/directives/permission.ts
  • Modify: ether-app-employee/src/main.ts

Step 1: 创建员工端权限指令

import type { Directive, DirectiveBinding } from 'vue'
import { usePermissionStore } from '@/stores/permission'

export const vPermission: Directive<HTMLElement, string> = {
  mounted(el: HTMLElement, binding: DirectiveBinding<string>) {
    const permissionStore = usePermissionStore()
    
    if (!permissionStore.hasPermissionCode(binding.value)) {
      el.parentNode?.removeChild(el)
    }
  }
}

export const vDisablePermission: Directive<HTMLElement, string> = {
  mounted(el: HTMLElement, binding: DirectiveBinding<string>) {
    const permissionStore = usePermissionStore()
    
    if (!permissionStore.hasPermissionCode(binding.value)) {
      el.setAttribute('disabled', 'true')
      el.classList.add('is-disabled')
      el.style.pointerEvents = 'none'
      el.style.opacity = '0.5'
    }
  }
}

Step 2: 在main.ts中注册指令

import { vPermission, vDisablePermission } from '@/directives/permission'

app.directive('permission', vPermission)
app.directive('disable-permission', vDisablePermission)

Step 3: 编译验证

cd ether-app-employee && npx vue-tsc --noEmit

Step 4: Commit

git add ether-app-employee/src/directives/permission.ts
git add ether-app-employee/src/main.ts
git commit -m "feat(app-employee): 新增权限指令"

Task 11: 数据库迁移脚本

Files:

  • Create: docs/08-DATABASE/permission-upgrade-v3.sql
  • Create: docs/08-DATABASE/permission-upgrade-v3-rollback.sql

Step 1: 创建升级脚本

-- permission-upgrade-v3.sql
-- 权限体系升级脚本 v3.0
-- 执行前请备份数据

BEGIN;

-- 1. 更新数据范围枚举PostgreSQL需要先删除再添加
-- 注意:需要确保没有使用旧枚举值的数据
ALTER TABLE auth_role ALTER COLUMN data_scope TYPE VARCHAR(20);
UPDATE auth_role SET data_scope = 'PROJECT' WHERE role_code IN ('PROPERTY_MANAGER', 'PROJECT_MANAGER', 'CS_STAFF');

-- 2. 新增角色
INSERT INTO auth_role (id, project_id, role_code, role_name, description, role_type, data_scope, enabled, sort_order, created_at, updated_at)
VALUES 
  (gen_random_uuid(), NULL, 'SYS_ADMIN', '系统管理员', '系统配置管理', 'SYSTEM', 'ALL', true, 2, NOW(), NOW()),
  (gen_random_uuid(), NULL, 'ENGINEERING_LEAD', '工程主管', '工程部管理', 'SYSTEM', 'DEPARTMENT', true, 3, NOW(), NOW()),
  (gen_random_uuid(), NULL, 'SECURITY_LEAD', '安保主管', '安保部管理', 'SYSTEM', 'DEPARTMENT', true, 4, NOW(), NOW()),
  (gen_random_uuid(), NULL, 'CLEANING_LEAD', '保洁主管', '保洁部管理', 'SYSTEM', 'DEPARTMENT', true, 5, NOW(), NOW()),
  (gen_random_uuid(), NULL, 'FINANCE_LEAD', '财务主管', '财务部管理', 'SYSTEM', 'DEPARTMENT', true, 6, NOW(), NOW()),
  (gen_random_uuid(), NULL, 'CLEANING_STAFF', '保洁人员', '保洁执行', 'SYSTEM', 'SELF', true, 9, NOW(), NOW())
ON CONFLICT (role_code, project_id) DO NOTHING;

-- 3. 更新现有角色
UPDATE auth_role SET 
  data_scope = 'PROJECT',
  role_name = '客服人员',
  description = '业主服务、访客核验'
WHERE role_code = 'CUSTOMER_SERVICE';

UPDATE auth_role SET role_code = 'CS_STAFF' WHERE role_code = 'CUSTOMER_SERVICE';

-- 4. 新增按钮级权限
INSERT INTO auth_permission (id, project_id, permission_code, permission_name, module, resource_type, action, permission_type, enabled, created_at, updated_at)
SELECT gen_random_uuid(), NULL, code, name, module, resource, action, 'BUTTON', true, NOW(), NOW()
FROM (VALUES
  -- 工单权限
  ('ops:work_order:view', '查看工单', 'ops', 'work_order', 'VIEW'),
  ('ops:work_order:create', '创建工单', 'ops', 'work_order', 'CREATE'),
  ('ops:work_order:assign', '分配工单', 'ops', 'work_order', 'ASSIGN'),
  ('ops:work_order:accept', '接单', 'ops', 'work_order', 'ACCEPT'),
  ('ops:work_order:start', '开始处理', 'ops', 'work_order', 'START'),
  ('ops:work_order:complete', '完成工单', 'ops', 'work_order', 'COMPLETE'),
  ('ops:work_order:transfer', '转单', 'ops', 'work_order', 'TRANSFER'),
  ('ops:work_order:close', '关闭工单', 'ops', 'work_order', 'CLOSE'),
  ('ops:work_order:report_fee', '填报费用', 'ops', 'work_order', 'REPORT_FEE'),
  ('ops:work_order:audit_fee', '费用审核', 'ops', 'work_order', 'AUDIT_FEE'),
  ('ops:work_order:audit_quality', '质量审核', 'ops', 'work_order', 'AUDIT_QUALITY'),
  ('ops:work_order:delete', '删除工单', 'ops', 'work_order', 'DELETE'),
  -- 巡检权限
  ('ops:inspection:view', '查看巡检', 'ops', 'inspection', 'VIEW'),
  ('ops:inspection:start', '开始巡检', 'ops', 'inspection', 'START'),
  ('ops:inspection:scan', '扫码签到', 'ops', 'inspection', 'SCAN'),
  ('ops:inspection:report', '异常上报', 'ops', 'inspection', 'REPORT'),
  ('ops:inspection:complete', '完成巡检', 'ops', 'inspection', 'COMPLETE'),
  ('ops:inspection:plan', '制定计划', 'ops', 'inspection', 'PLAN'),
  ('ops:inspection:assign', '指派任务', 'ops', 'inspection', 'ASSIGN'),
  ('ops:inspection:force_close', '强制闭环', 'ops', 'inspection', 'FORCE_CLOSE'),
  -- 设备权限
  ('mdm:equipment:view', '查看设备', 'mdm', 'equipment', 'VIEW'),
  ('mdm:equipment:scan', '扫码巡检', 'mdm', 'equipment', 'SCAN'),
  ('mdm:equipment:maintain', '维护记录', 'mdm', 'equipment', 'MAINTAIN'),
  ('mdm:equipment:edit', '编辑设备', 'mdm', 'equipment', 'EDIT'),
  ('mdm:equipment:create', '新增设备', 'mdm', 'equipment', 'CREATE'),
  ('mdm:equipment:delete', '删除设备', 'mdm', 'equipment', 'DELETE'),
  -- 访客权限
  ('ops:visitor:view', '查看访客', 'ops', 'visitor', 'VIEW'),
  ('ops:visitor:register', '访客登记', 'ops', 'visitor', 'REGISTER'),
  ('ops:visitor:verify', '访客核验', 'ops', 'visitor', 'VERIFY'),
  ('ops:visitor:release', '访客放行', 'ops', 'visitor', 'RELEASE'),
  ('ops:visitor:export', '导出记录', 'ops', 'visitor', 'EXPORT'),
  -- 财务权限
  ('finance:bill:view', '查看账单', 'finance', 'bill', 'VIEW'),
  ('finance:bill:create', '生成账单', 'finance', 'bill', 'CREATE'),
  ('finance:bill:adjust', '调整账单', 'finance', 'bill', 'ADJUST'),
  ('finance:bill:collect', '收费登记', 'finance', 'bill', 'COLLECT'),
  ('finance:bill:pay', '在线缴费', 'finance', 'bill', 'PAY'),
  ('finance:bill:audit', '收费审核', 'finance', 'bill', 'AUDIT'),
  ('finance:bill:reduce', '减免审批', 'finance', 'bill', 'REDUCE'),
  ('finance:bill:export', '导出报表', 'finance', 'bill', 'EXPORT')
) AS t(code, name, module, resource, action)
ON CONFLICT (permission_code, project_id) DO NOTHING;

-- 5. 为现有角色分配新权限(根据角色类型)
-- 超级管理员:所有权限
INSERT INTO auth_role_permission (id, role_id, permission_id, created_at)
SELECT gen_random_uuid(), r.id, p.id, NOW()
FROM auth_role r, auth_permission p
WHERE r.role_code = 'SUPER_ADMIN' AND p.project_id IS NULL
ON CONFLICT (role_id, permission_id) DO NOTHING;

COMMIT;

Step 2: 创建回滚脚本

-- permission-upgrade-v3-rollback.sql
-- 权限体系升级回滚脚本 v3.0

BEGIN;

-- 1. 删除新增的角色
DELETE FROM auth_role WHERE role_code IN ('SYS_ADMIN', 'ENGINEERING_LEAD', 'SECURITY_LEAD', 'CLEANING_LEAD', 'FINANCE_LEAD', 'CLEANING_STAFF');

-- 2. 恢复原有角色设置
UPDATE auth_role SET 
  data_scope = 'DEPARTMENT',
  role_name = '客服人员',
  role_code = 'CUSTOMER_SERVICE'
WHERE role_code = 'CS_STAFF';

-- 3. 删除新增的按钮权限
DELETE FROM auth_permission WHERE permission_code LIKE 'ops:work_order:%' AND permission_type = 'BUTTON';
DELETE FROM auth_permission WHERE permission_code LIKE 'ops:inspection:%' AND permission_type = 'BUTTON';
DELETE FROM auth_permission WHERE permission_code LIKE 'mdm:equipment:%' AND permission_type = 'BUTTON';
DELETE FROM auth_permission WHERE permission_code LIKE 'ops:visitor:%' AND permission_type = 'BUTTON';
DELETE FROM auth_permission WHERE permission_code LIKE 'finance:bill:%' AND permission_type = 'BUTTON';

-- 4. 删除角色权限关联
DELETE FROM auth_role_permission WHERE permission_id NOT IN (SELECT id FROM auth_permission);

COMMIT;

Step 3: Commit

git add docs/08-DATABASE/permission-upgrade-v3.sql
git add docs/08-DATABASE/permission-upgrade-v3-rollback.sql
git commit -m "feat(db): 新增权限升级迁移脚本"

Task 12: 后端单元测试

Files:

  • Create: ether-auth/src/test/java/com/ether/auth/service/PermissionServiceV3Test.java

Step 1: 创建测试类

package com.ether.auth.service;

import com.ether.auth.entity.Permission;
import com.ether.auth.entity.Role;
import com.ether.auth.repository.PermissionRepository;
import com.ether.auth.repository.RoleRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Set;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
class PermissionServiceV3Test {

    @Autowired
    private PermissionService permissionService;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private PermissionRepository permissionRepository;

    private UUID testUserId;
    private UUID testProjectId;

    @BeforeEach
    void setUp() {
        testUserId = UUID.randomUUID();
        testProjectId = UUID.randomUUID();
    }

    @Test
    @DisplayName("测试数据范围枚举包含PROJECT")
    void testDataScopeContainsProject() {
        Role.DataScope[] scopes = Role.DataScope.values();
        assertTrue(List.of(scopes).contains(Role.DataScope.PROJECT));
    }

    @Test
    @DisplayName("测试新角色模板存在")
    void testNewRoleTemplatesExist() {
        List<String> expectedRoles = List.of(
            "SYS_ADMIN", "ENGINEERING_LEAD", "SECURITY_LEAD", 
            "CLEANING_LEAD", "FINANCE_LEAD", "CLEANING_STAFF"
        );
        
        for (String roleCode : expectedRoles) {
            assertTrue(roleRepository.findByRoleCode(roleCode).isPresent() || 
                       roleRepository.findByRoleCodeAndProjectId(roleCode, null).isPresent(),
                       "Role " + roleCode + " should exist");
        }
    }

    @Test
    @DisplayName("测试按钮级权限编码存在")
    void testButtonPermissionsExist() {
        List<String> expectedPermissions = List.of(
            "ops:work_order:assign",
            "ops:work_order:accept",
            "ops:work_order:complete",
            "ops:inspection:start",
            "ops:inspection:scan",
            "mdm:equipment:scan",
            "ops:visitor:register",
            "finance:bill:audit"
        );
        
        for (String permCode : expectedPermissions) {
            assertTrue(permissionRepository.findByPermissionCode(permCode).isPresent() ||
                       permissionRepository.findByPermissionCodeAndProjectId(permCode, null).isPresent(),
                       "Permission " + permCode + " should exist");
        }
    }

    @Test
    @DisplayName("测试状态驱动权限检查")
    void testStateDrivenPermission() {
        // 测试 CREATED 状态允许的操作
        Set<String> createdActions = Set.of("ASSIGN", "EDIT", "DELETE");
        
        // 测试 ASSIGNED 状态允许的操作
        Set<String> assignedActions = Set.of("ACCEPT", "TRANSFER", "SUSPEND", "RETURN");
        
        // 测试 CLOSED 状态不允许任何操作
        Set<String> closedActions = Set.of();
        
        // 实际测试需要mock用户和权限
        // 这里只验证方法存在
        assertDoesNotThrow(() -> {
            permissionService.getClass().getMethod("hasActionPermission", 
                UUID.class, UUID.class, String.class, String.class, String.class, String.class);
        });
    }
}

Step 2: 运行测试

cd ether-auth && mvn test -Dtest=PermissionServiceV3Test

Step 3: Commit

git add ether-auth/src/test/java/com/ether/auth/service/PermissionServiceV3Test.java
git commit -m "test(auth): 新增权限服务V3单元测试"

Task 13: 前端单元测试

Files:

  • Create: ether-ui-admin/src/__tests__/permissionStore.test.ts

Step 1: 创建测试文件

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { usePermissionStore } from '@/stores/permission'

describe('Permission Store V3', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  describe('hasPermissionCode', () => {
    it('should return true when permission exists', () => {
      const store = usePermissionStore()
      // Mock permissions
      store.permissions = [
        { code: 'ops:work_order:view', name: '查看工单', type: 'BUTTON' },
        { code: 'ops:work_order:create', name: '创建工单', type: 'BUTTON' }
      ]
      
      expect(store.hasPermissionCode('ops:work_order:view')).toBe(true)
      expect(store.hasPermissionCode('ops:work_order:edit')).toBe(false)
    })
  })

  describe('hasAnyPermission', () => {
    it('should return true when any permission exists', () => {
      const store = usePermissionStore()
      store.permissions = [
        { code: 'ops:work_order:view', name: '查看工单', type: 'BUTTON' }
      ]
      
      expect(store.hasAnyPermission(['ops:work_order:view', 'ops:work_order:edit'])).toBe(true)
      expect(store.hasAnyPermission(['ops:work_order:create', 'ops:work_order:edit'])).toBe(false)
    })
  })

  describe('checkActionPermission', () => {
    it('should call API with correct parameters', async () => {
      const store = usePermissionStore()
      
      // Mock request
      vi.mock('@/utils/request', () => ({
        request: {
          post: vi.fn().mockResolvedValue({ data: true })
        }
      }))
      
      const result = await store.checkActionPermission({
        permission: 'ops:work_order:assign',
        status: 'CREATED',
        action: 'ASSIGN',
        entityType: 'work_order'
      })
      
      expect(result).toBeDefined()
    })
  })
})

Step 2: 运行测试

cd ether-ui-admin && npm run test -- src/__tests__/permissionStore.test.ts

Step 3: Commit

git add ether-ui-admin/src/__tests__/permissionStore.test.ts
git commit -m "test(ui-admin): 新增权限Store单元测试"

Task 14: E2E测试方案

Files:

  • Create: docs/07-TESTING/plans/PERMISSION_UPGRADE_E2E_TEST_PLAN.md

Step 1: 创建E2E测试计划

# 权限体系升级 E2E 测试计划

## 一、测试范围

### 1.1 测试端覆盖
- 管理后台 (ether-ui-admin)
- 员工端 (ether-app-employee)
- 业主端 (ether-app-owner)

### 1.2 测试场景覆盖
- 角色权限验证
- 按钮级权限验证
- 状态驱动权限验证
- 数据范围权限验证

## 二、测试用例

### 2.1 管理后台测试用例

| 用例ID | 测试场景 | 测试步骤 | 预期结果 |
|--------|---------|---------|---------|
| E2E-PERM-001 | 超级管理员登录 | 1. 使用超级管理员账号登录<br>2. 检查菜单显示<br>3. 检查按钮权限 | 显示所有菜单和按钮 |
| E2E-PERM-002 | 项目经理登录 | 1. 使用项目经理账号登录<br>2. 检查菜单显示<br>3. 检查按钮权限 | 显示项目管理相关菜单 |
| E2E-PERM-003 | 工程主管登录 | 1. 使用工程主管账号登录<br>2. 检查工单管理权限<br>3. 检查设备管理权限 | 可分配工单、管理设备 |
| E2E-PERM-004 | 维修人员登录 | 1. 使用维修人员账号登录<br>2. 检查工单操作权限<br>3. 检查状态驱动权限 | 可接单、开始、完成工单 |
| E2E-PERM-005 | 工单状态驱动权限 | 1. 创建工单<br>2. 检查CREATED状态可用按钮<br>3. 分配工单<br>4. 检查ASSIGNED状态可用按钮 | 按钮根据状态动态显示 |

### 2.2 员工端测试用例

| 用例ID | 测试场景 | 测试步骤 | 预期结果 |
|--------|---------|---------|---------|
| E2E-PERM-010 | 维修人员移动端登录 | 1. 使用维修人员账号登录<br>2. 检查首页显示<br>3. 检查工单列表 | 显示分配给自己的工单 |
| E2E-PERM-011 | 安保人员移动端登录 | 1. 使用安保人员账号登录<br>2. 检查巡检功能<br>3. 检查访客管理功能 | 可执行巡检、核验访客 |
| E2E-PERM-012 | 保洁人员移动端登录 | 1. 使用保洁人员账号登录<br>2. 检查巡检功能<br>3. 检查任务列表 | 显示分配给自己的任务 |

### 2.3 业主端测试用例

| 用例ID | 测试场景 | 测试步骤 | 预期结果 |
|--------|---------|---------|---------|
| E2E-PERM-020 | 业主登录 | 1. 使用业主账号登录<br>2. 检查首页显示<br>3. 检查功能权限 | 显示报修、缴费、访客邀请功能 |
| E2E-PERM-021 | 业主报修 | 1. 提交报修工单<br>2. 查看工单进度<br>3. 完成后评价 | 可提交、查看、评价工单 |
| E2E-PERM-022 | 业主缴费 | 1. 查看账单列表<br>2. 在线缴费<br>3. 查看缴费记录 | 可查看、缴费、查看记录 |

## 三、测试数据准备

### 3.1 测试账号

| 角色 | 用户名 | 密码 | 用途 |
|------|--------|------|------|
| 超级管理员 | super_admin | Test@123 | 全权限测试 |
| 项目经理 | project_manager | Test@123 | 项目管理测试 |
| 工程主管 | engineering_lead | Test@123 | 工单管理测试 |
| 维修人员 | maintenance_staff | Test@123 | 工单执行测试 |
| 安保人员 | security_staff | Test@123 | 巡检验证测试 |
| 保洁人员 | cleaning_staff | Test@123 | 保洁任务测试 |
| 客服人员 | cs_staff | Test@123 | 服务支持测试 |
| 业主 | owner | Test@123 | 业主功能测试 |

### 3.2 测试项目数据
- 项目测试小区A
- 楼栋1号楼、2号楼
- 房间101-501
- 设备:电梯、门禁、监控等

## 四、测试执行

### 4.1 执行顺序
1. 后端服务启动验证
2. 数据库迁移执行
3. 管理后台测试
4. 员工端测试
5. 业主端测试
6. 回归测试

### 4.2 缺陷等级定义
- P0核心功能不可用阻塞测试
- P1主要功能异常影响业务流程
- P2次要功能异常不影响主流程
- P3UI/体验问题

## 五、测试报告模板

### 测试执行摘要
- 测试时间:
- 测试人员:
- 测试环境:
- 测试结果:通过/失败

### 缺陷统计
| 等级 | 发现数 | 已修复 | 待修复 |
|------|--------|--------|--------|
| P0 | | | |
| P1 | | | |
| P2 | | | |
| P3 | | | |

### 测试结论
- 是否满足上线条件:
- 遗留问题:
- 建议:

Step 2: Commit

git add docs/07-TESTING/plans/PERMISSION_UPGRADE_E2E_TEST_PLAN.md
git commit -m "docs(test): 新增权限升级E2E测试计划"

Task 15: 最终验证与文档更新

Files:

  • Modify: docs/01-REQUIREMENTS/PRODUCT_REQUIREMENTS.md
  • Modify: docs/02-DESIGN/domains/05-AUTH.md

Step 1: 验证所有服务启动

# 启动后端服务
cd ether-auth && mvn spring-boot:run &
cd ether-mdm && mvn spring-boot:run &
cd ether-ops && mvn spring-boot:run &
cd ether-gateway && mvn spring-boot:run &

# 启动前端服务
cd ether-ui-admin && npm run dev &
cd ether-app-employee && npm run dev &
cd ether-app-owner && npm run dev &

Step 2: 执行数据库迁移

psql -U ether -d ether_db -f docs/08-DATABASE/permission-upgrade-v3.sql

Step 3: 运行所有测试

# 后端测试
cd ether-auth && mvn test
cd ether-ops && mvn test

# 前端测试
cd ether-ui-admin && npm run test
cd ether-app-employee && npm run test

Step 4: 更新文档版本

更新 PRODUCT_REQUIREMENTS.md 的修订记录:

| v3.0 | 2026-02-27 | - | 完善角色权限矩阵13个角色体系、按钮级权限、状态驱动权限 |
| v3.1 | 2026-02-27 | - | 权限体系代码升级完成新增7个角色、40+按钮权限 |

Step 5: Commit

git add docs/01-REQUIREMENTS/PRODUCT_REQUIREMENTS.md
git add docs/02-DESIGN/domains/05-AUTH.md
git commit -m "docs: 更新权限升级完成文档"

四、风险评估与应对

4.1 风险列表

风险项 影响 概率 应对措施
数据迁移失败 提供回滚脚本,备份原数据
现有功能受影响 充分测试,渐进式发布
前端兼容性问题 保持向后兼容,提供降级方案
性能下降 权限缓存优化
用户角色数据丢失 迁移前备份,迁移后验证

4.2 回滚方案

  1. 代码回滚: Git revert 到升级前版本
  2. 数据回滚: 执行 permission-upgrade-v3-rollback.sql
  3. 服务重启: 重启所有后端服务

五、验收标准

5.1 功能验收

  • 所有13个角色可正常创建和使用
  • 所有40+按钮权限可正常检查
  • 状态驱动权限正确工作
  • 数据范围权限正确过滤
  • 三端权限检查正常工作

5.2 兼容性验收

  • 现有用户可正常登录
  • 现有功能不受影响
  • 现有数据完整保留

5.3 性能验收

  • 权限检查响应时间 < 100ms
  • 页面加载时间无明显增加
  • 数据库查询性能正常

5.4 测试验收

  • 单元测试覆盖率 > 80%
  • 集成测试全部通过
  • E2E测试全部通过
  • 无P0/P1级别缺陷

Plan complete and saved to docs/specs/permission-upgrade-v3-spec/spec.md. Two execution options:

1. Subagent-Driven (this session) - I dispatch fresh subagent per task, review between tasks, fast iteration

2. Parallel Session (separate) - Open new session with executing-plans, batch execution with checkpoints

Which approach?