# 权限与账户领域技术方案

**领域编号**: 4.5  
**微服务**: ether-auth  
**最后更新**: 2026-02-27

---

## 一、领域概述

### 1.1 领域职责

权限与账户领域负责 Ether 平台的身份认证与授权管理：
- 用户认证与登录
- RBAC 权限模型（角色-权限-用户关联）
- 项目隔离与数据权限
- 按钮级权限控制
- 状态驱动权限
- 访客凭证管理
- 操作日志审计

### 1.2 核心概念

| 概念 | 说明 | 对应实体 |
|------|------|----------|
| **用户** | 系统登录账户 | User |
| **角色** | 权限集合 | Role |
| **权限** | 功能访问控制点 | Permission |
| **项目** | 多租户隔离单位 | Project |
| **访客凭证** | 临时访问授权 | VisitorCredential |

---

## 二、角色体系设计

### 2.1 角色总览

| 类型 | 角色编码 | 角色名称 | 数据范围 | 终端类型 | 一线人员 |
|------|---------|---------|---------|---------|---------|
| **系统级** |
| 系统管理 | SUPER_ADMIN | 超级管理员 | ALL | ALL | 否 |
| 系统管理 | SYS_ADMIN | 系统管理员 | ALL | ADMIN_ONLY | 否 |
| **项目管理** |
| 项目管理 | PROPERTY_MANAGER | 物业经理 | PROJECT | ALL | 否 |
| 项目管理 | PROJECT_MANAGER | 项目经理 | PROJECT | ALL | 否 |
| **部门主管** |
| 工程管理 | ENGINEERING_LEAD | 工程主管 | DEPARTMENT | ALL | 否 |
| 安保管理 | SECURITY_LEAD | 安保主管 | DEPARTMENT | ALL | 否 |
| 保洁管理 | CLEANING_LEAD | 保洁主管 | DEPARTMENT | ALL | 否 |
| 财务管理 | FINANCE_LEAD | 财务主管 | DEPARTMENT | ALL | 否 |
| **一线执行** |
| 工程执行 | MAINTENANCE_STAFF | 维修人员 | SELF | MOBILE_ONLY | 是 |
| 安保执行 | SECURITY_STAFF | 安保人员 | SELF | MOBILE_ONLY | 是 |
| 保洁执行 | CLEANING_STAFF | 保洁人员 | SELF | MOBILE_ONLY | 是 |
| **服务支持** |
| 业主服务 | CS_STAFF | 客服人员 | PROJECT | ALL | 否 |
| **外部用户** |
| 业主 | OWNER | 业主 | SELF | OWNER_APP | 否 |

### 2.2 角色职责说明

#### 2.2.1 系统级角色

| 角色 | 职责范围 | 典型场景 |
|------|---------|---------|
| **超级管理员** | 系统最高权限，管理所有项目和用户 | 系统初始化、用户创建、项目创建 |
| **系统管理员** | 系统配置管理，不参与业务操作 | 系统参数配置、日志查看 |

#### 2.2.2 项目管理角色

| 角色 | 职责范围 | 典型场景 |
|------|---------|---------|
| **物业经理** | 项目经营第一负责人，拥有本项目全部权限 | 项目整体运营、费用审核、合同审批 |
| **项目经理** | 单项目管理，拥有本项目全部业务权限 | 工单管理、人员分配、报表查看 |

#### 2.2.3 部门主管角色

| 角色 | 管理模块 | 职责范围 | 典型场景 |
|------|---------|---------|---------|
| **工程主管** | 工单、设备、工程巡检 | 本部门工程事务管理 | 工单分配、设备管理、巡检计划制定 |
| **安保主管** | 安保巡检、访客管理 | 本部门安保事务管理 | 安保巡检计划、访客核验监督 |
| **保洁主管** | 保洁巡检、品质检查 | 本部门保洁事务管理 | 保洁巡检计划、品质检查 |
| **财务主管** | 账单、收费、费用审核 | 本部门财务事务管理 | 账单生成、费用审核、报表导出 |

#### 2.2.4 一线执行角色

| 角色 | 执行范围 | 终端限制 | 典型场景 |
|------|---------|---------|---------|
| **维修人员** | 工单处理、设备巡检 | 仅移动端 | 接单、维修、填报费用、设备巡检 |
| **安保人员** | 安保巡检、访客核验 | 仅移动端 | 扫码签到、访客核验、异常报送 |
| **保洁人员** | 保洁巡检、品质检查 | 仅移动端 | 扫码签到、品质异常报送 |

#### 2.2.5 服务支持角色

| 角色 | 职责范围 | 典型场景 |
|------|---------|---------|
| **客服人员** | 业主服务接口，不参与工单流程 | 业主咨询、资料维护、访客辅助核验 |

#### 2.2.6 外部用户角色

| 角色 | 权限范围 | 典型场景 |
|------|---------|---------|
| **业主** | 本人房产相关数据 | 在线报修、缴费、访客邀请、评价 |

---

## 三、领域模型

### 3.1 聚合根设计

#### User（系统用户）

```java
@Entity
@Table(name = "auth_user")
@Data
public class User {
    @Id
    private UUID id;
    
    private String username;       // 登录账号
    private String password;       // 加密密码
    private String salt;           // 密码盐值
    
    private String realName;       // 真实姓名
    private String phone;
    private String email;
    private String avatar;         // 头像URL
    
    private UserStatus status;     // ACTIVE/LOCKED/DISABLED
    private LocalDateTime lastLoginTime;
    private String lastLoginIp;
    
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "auth_user_role",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private List<Role> roles;
    
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private UUID createdBy;
}

public enum UserStatus {
    ACTIVE("正常"),
    LOCKED("锁定"),
    DISABLED("禁用");
}
```

#### Role（角色）

```java
@Entity
@Table(name = "auth_role")
@Data
public class Role {
    @Id
    private UUID id;
    private UUID projectId;        // 项目隔离（NULL表示系统级）
    
    private String name;           // 角色名称
    private String code;           // 角色编码
    private String description;    // 角色描述
    
    private RoleType type;         // SYSTEM(系统预设)/CUSTOM(自定义)
    
    // 数据权限范围（四级）
    private DataScope dataScope;   // ALL/PROJECT/DEPARTMENT/SELF
    
    // 新增：业务属性
    private BusinessType businessType;     // 业务类型
    private TerminalType terminalType;     // 终端类型限制
    private Boolean isFrontline;           // 是否一线执行人员
    
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "auth_role_permission",
        joinColumns = @JoinColumn(name = "role_id"),
        inverseJoinColumns = @JoinColumn(name = "permission_id")
    )
    private List<Permission> permissions;
    
    private Boolean enabled;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

public enum RoleType {
    SYSTEM("系统预设"),
    CUSTOM("自定义");
}

// 数据权限范围枚举（四级）
public enum DataScope {
    ALL("全部数据"),
    PROJECT("本项目数据"),
    DEPARTMENT("本部门数据"),
    SELF("仅本人数据");
}

// 业务类型枚举
public enum BusinessType {
    MANAGEMENT("管理类"),
    ENGINEERING("工程类"),
    SECURITY("安保类"),
    CLEANING("保洁类"),
    CUSTOMER_SERVICE("客服类"),
    FINANCE("财务类");
}

// 终端类型枚举
public enum TerminalType {
    ALL("全终端"),
    ADMIN_ONLY("仅管理后台"),
    MOBILE_ONLY("仅移动端"),
    OWNER_APP("业主端");
}
```

#### Permission（权限）

```java
@Entity
@Table(name = "auth_permission")
@Data
public class Permission {
    @Id
    private UUID id;
    private UUID projectId;        // 项目隔离（NULL表示系统级）
    
    private String name;           // 权限名称
    private String code;           // 权限编码: module:resource:action
    private PermissionType type;   // MENU/BUTTON/API
    
    // 模块和资源信息
    private String module;         // 模块标识: system/mdm/ops/finance
    private String resource;       // 资源类型: user/role/work_order
    private String action;         // 操作类型: view/create/edit/delete/assign
    
    // 菜单属性
    private String path;           // 路由路径
    private String component;      // 组件路径
    private String icon;           // 图标
    private Integer sortOrder;     // 排序
    
    // 树形结构
    private UUID parentId;         // 父权限ID
    private Integer level;         // 层级
    
    // API属性
    private String apiMethod;      // GET/POST/PUT/DELETE
    private String apiPath;        // API路径
    
    private Boolean enabled;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

public enum PermissionType {
    MENU("菜单"),
    BUTTON("按钮"),
    API("接口");
}
```

### 3.2 关联实体

#### UserRole（用户角色关联）

```java
@Entity
@Table(name = "auth_user_role")
@Data
public class UserRole {
    @Id
    private UUID id;
    
    private UUID userId;
    private UUID roleId;
    private UUID projectId;        // 项目隔离
    private Boolean isDefault;     // 是否默认角色
    
    private LocalDateTime createdAt;
}
```

#### RolePermission（角色权限关联）

```java
@Entity
@Table(name = "auth_role_permission")
@Data
public class RolePermission {
    @Id
    private UUID id;
    
    private UUID roleId;
    private UUID permissionId;
    
    private LocalDateTime createdAt;
}
```

---

## 四、权限编码规范

### 4.1 编码格式

**格式**: `module:resource:action`

| 组成部分 | 说明 | 示例值 |
|---------|------|--------|
| module | 模块标识 | system, mdm, ops, asset, finance |
| resource | 资源类型 | user, role, work_order, equipment, bill |
| action | 操作类型 | view, create, edit, delete, assign, accept, complete |

### 4.2 操作类型说明

| 操作类型 | 说明 | 适用场景 |
|---------|------|---------|
| view | 查看 | 列表、详情查看 |
| create | 创建 | 新增记录 |
| edit | 编辑 | 修改记录 |
| delete | 删除 | 删除记录 |
| assign | 分配 | 工单分配、任务分配 |
| accept | 接单 | 工单接单 |
| start | 开始 | 开始处理、开始巡检 |
| complete | 完成 | 完成工单、完成巡检 |
| transfer | 转单 | 工单转单 |
| close | 关闭 | 关闭工单 |
| evaluate | 评价 | 工单评价 |
| report_fee | 填报费用 | 工单费用填报 |
| audit_fee | 费用审核 | 费用审核 |
| audit_quality | 质量审核 | 质量审核 |
| export | 导出 | 数据导出 |
| generate_qr | 生成二维码 | 二维码生成 |
| scan_view | 扫码查看 | 扫码查看设备 |
| force_close | 强制闭环 | 逾期任务强制关闭 |
| verify | 核验 | 访客核验 |

---

## 五、认证授权流程

### 5.1 JWT Token 认证

```java
@Component
public class JwtTokenProvider {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.expiration:86400000}")
    private long jwtExpiration;
    
    private SecretKey secretKey;
    
    @PostConstruct
    public void init() {
        this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
    }
    
    public String generateToken(User user, UUID projectId) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpiration);
        
        return Jwts.builder()
            .setSubject(user.getId().toString())
            .claim("username", user.getUsername())
            .claim("realName", user.getRealName())
            .claim("projectId", projectId != null ? projectId.toString() : null)
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(secretKey, SignatureAlgorithm.HS256)
            .compact();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    public UUID getUserIdFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(token)
            .getBody();
        
        return UUID.fromString(claims.getSubject());
    }
    
    public UUID getProjectIdFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(token)
            .getBody();
        
        String projectId = claims.get("projectId", String.class);
        return projectId != null ? UUID.fromString(projectId) : null;
    }
}
```

### 5.2 项目上下文

```java
public class ProjectContextHolder {
    private static final ThreadLocal<UUID> PROJECT_CONTEXT = new ThreadLocal<>();
    
    public static void setCurrentProjectId(UUID projectId) {
        PROJECT_CONTEXT.set(projectId);
    }
    
    public static UUID getCurrentProjectId() {
        return PROJECT_CONTEXT.get();
    }
    
    public static void clear() {
        PROJECT_CONTEXT.remove();
    }
}

@Component
public class ProjectContextInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String projectId = request.getHeader("X-Project-ID");
        if (StringUtils.hasText(projectId)) {
            try {
                ProjectContextHolder.setCurrentProjectId(UUID.fromString(projectId));
            } catch (IllegalArgumentException e) {
                // 无效的Project ID格式
            }
        }
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ProjectContextHolder.clear();
    }
}
```

### 5.3 权限检查

```java
@Service
public class PermissionService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private RoleRepository roleRepository;
    
    // 检查用户是否有指定权限
    public boolean hasPermission(UUID userId, String permissionCode) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new NotFoundException("用户不存在"));
        
        // 超级管理员拥有所有权限
        if (isSuperAdmin(user)) {
            return true;
        }
        
        // 检查用户角色是否包含该权限
        for (Role role : user.getRoles()) {
            if (!role.getEnabled()) {
                continue;
            }
            for (Permission permission : role.getPermissions()) {
                if (permission.getCode().equals(permissionCode)) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    // 检查用户是否有指定权限（带项目隔离）
    public boolean hasPermission(UUID userId, UUID projectId, String permissionCode) {
        // 超级管理员
        if (isSuperAdmin(userId)) {
            return true;
        }
        
        // 获取用户在该项目下的权限
        List<Permission> permissions = permissionRepository
            .findPermissionsByUserIdAndProjectId(userId, projectId);
        
        return permissions.stream()
            .anyMatch(p -> p.getCode().equals(permissionCode));
    }
    
    // 获取用户菜单
    public List<Permission> getUserMenus(UUID userId, UUID projectId) {
        List<Permission> permissions = permissionRepository
            .findByUserIdAndProjectId(userId, projectId);
        
        return permissions.stream()
            .filter(p -> p.getType() == PermissionType.MENU)
            .filter(Permission::getEnabled)
            .sorted(Comparator.comparing(Permission::getSortOrder))
            .collect(Collectors.toList());
    }
}
```

---

## 六、数据权限设计

### 6.1 数据范围级别

| 数据范围 | 编码 | 说明 | SQL预设 |
|---------|------|------|---------|
| 全部 | ALL | 所有项目数据 | `WHERE 1=1` |
| 项目 | PROJECT | 本项目数据 | `WHERE project_id = $current_project` |
| 部门 | DEPARTMENT | 本部门数据 | `WHERE project_id = $current_project AND dept_id = $current_dept` |
| 本人 | SELF | 仅本人数据 | `WHERE assigned_to = $current_user` |

### 6.2 数据权限工具类

```java
public final class DataScopeHelper {
    
    public static String getDataFilterSql(String tableAlias, String creatorField, 
                                          String deptField, String projectField) {
        DataScopeInfo scope = DataScopeContext.getDataScopeInfo();
        String prefix = tableAlias != null ? tableAlias + "." : "";
        
        return switch (scope.getScope()) {
            case ALL -> "1=1";
            case PROJECT -> prefix + projectField + " = '" + scope.getProjectId() + "'";
            case DEPARTMENT -> prefix + projectField + " = '" + scope.getProjectId() + "' " +
                              "AND " + prefix + deptField + " = '" + scope.getDepartmentId() + "'";
            case SELF -> prefix + creatorField + " = '" + scope.getUserId() + "'";
        };
    }
    
    public static boolean canAccessData(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());
        };
    }
}
```

---

## 七、状态驱动权限

### 7.1 工单状态与操作映射

```java
public enum WorkOrderStatus {
    CREATED("已创建"),
    ASSIGNED("已分配"),
    ACCEPTED("已接单"),
    IN_PROGRESS("处理中"),
    PENDING("待确认"),
    COMPLETED("已完成"),
    CLOSED("已关闭"),
    SUSPENDED("已挂起"),
    RETURNED("已退回");
    
    // 获取该状态下可执行的操作
    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 enum WorkOrderAction {
    VIEW, CREATE, EDIT, DELETE,
    ASSIGN, ACCEPT, START, COMPLETE, TRANSFER, CLOSE, SUSPEND, RESUME, RETURN,
    EVALUATE, REPORT_FEE, AUDIT_FEE, AUDIT_QUALITY
}
```

### 7.2 权限校验增强

```java
// WorkOrderServiceImpl.java
public WorkOrder assign(UUID workOrderId, UUID assigneeId, String assigneeName) {
    WorkOrder workOrder = getWorkOrder(workOrderId);
    UUID currentUserId = getCurrentUserId();
    
    // 1. 状态校验
    validateStatusTransition(workOrder.getStatus(), WorkOrderStatus.ASSIGNED);
    
    // 2. 权限校验
    if (!hasActionPermission(workOrder, WorkOrderAction.ASSIGN)) {
        throw new PermissionDeniedException("无分配工单权限");
    }
    
    // 3. 数据权限校验
    if (!canAccessWorkOrder(workOrder)) {
        throw new PermissionDeniedException("无权访问该工单");
    }
    
    // 执行分配逻辑...
}

private boolean hasActionPermission(WorkOrder workOrder, WorkOrderAction action) {
    // 1. 检查状态是否允许该操作
    if (!workOrder.getStatus().getAllowedActions().contains(action)) {
        return false;
    }
    
    // 2. 检查用户是否有该操作的权限编码
    String permissionCode = "ops:work_order:" + action.name().toLowerCase();
    return permissionService.hasPermission(getCurrentUserId(), getProjectId(), permissionCode);
}

private boolean canAccessWorkOrder(WorkOrder workOrder) {
    DataScopeInfo scope = dataScopeContext.getDataScopeInfo();
    
    return switch (scope.getScope()) {
        case ALL -> true;
        case PROJECT -> workOrder.getProjectId().equals(scope.getProjectId());
        case DEPARTMENT -> workOrder.getDepartmentId().equals(scope.getDepartmentId());
        case SELF -> workOrder.getAssigneeId().equals(scope.getUserId()) ||
                     workOrder.getCreatorId().equals(scope.getUserId());
    };
}
```

---

## 八、访客凭证管理

### 8.1 访客凭证设计

```java
@Entity
@Table(name = "auth_visitor_credential")
@Data
public class VisitorCredential {
    @Id
    private UUID id;
    private UUID projectId;
    
    private String visitorName;
    private String visitorPhone;
    private String visitorIdCard;
    
    private String credentialCode;
    private String qrCode;
    private LocalDateTime expireTime;
    
    private UUID spaceNodeId;
    private String accessGates;
    
    private CredentialStatus status;
    
    private LocalDateTime firstUseTime;
    private LocalDateTime lastUseTime;
    private Integer useCount;
    
    private UUID creatorId;
    private String creatorName;
    
    private LocalDateTime createdAt;
}

public enum CredentialStatus {
    ACTIVE("有效"),
    USED("已使用"),
    EXPIRED("已过期"),
    REVOKED("已撤销");
}
```

### 8.2 二维码生成与验证

```java
@Service
public class VisitorCredentialService {
    
    @Autowired
    private VisitorCredentialRepository credentialRepository;
    
    public VisitorCredential generateCredential(VisitorCredentialRequest request) {
        VisitorCredential credential = new VisitorCredential();
        credential.setId(UUID.randomUUID());
        credential.setProjectId(request.getProjectId());
        credential.setVisitorName(request.getVisitorName());
        credential.setVisitorPhone(request.getVisitorPhone());
        credential.setVisitorIdCard(request.getVisitorIdCard());
        credential.setSpaceNodeId(request.getSpaceNodeId());
        credential.setAccessGates(JsonUtils.toJson(request.getAccessGates()));
        credential.setStatus(CredentialStatus.ACTIVE);
        credential.setExpireTime(LocalDateTime.now().plusHours(24));
        credential.setUseCount(0);
        credential.setCreatorId(request.getCreatorId());
        credential.setCreatedAt(LocalDateTime.now());
        
        String qrContent = generateQrContent(credential);
        credential.setQrCode(qrContent);
        
        return credentialRepository.save(credential);
    }
    
    private String generateQrContent(VisitorCredential credential) {
        String data = String.format("VC:%s:%d", 
            credential.getId(), 
            System.currentTimeMillis());
        
        String signature = generateSignature(data);
        return data + ":" + signature;
    }
    
    public CredentialVerifyResult verifyCredential(String qrCode, String gateCode) {
        String[] parts = qrCode.split(":");
        if (parts.length != 4 || !"VC".equals(parts[0])) {
            return CredentialVerifyResult.fail("无效的二维码格式");
        }
        
        UUID credentialId = UUID.fromString(parts[1]);
        
        VisitorCredential credential = credentialRepository.findById(credentialId)
            .orElse(null);
        
        if (credential == null) {
            return CredentialVerifyResult.fail("凭证不存在");
        }
        
        if (credential.getStatus() == CredentialStatus.EXPIRED ||
            credential.getExpireTime().isBefore(LocalDateTime.now())) {
            return CredentialVerifyResult.fail("凭证已过期");
        }
        
        if (credential.getStatus() == CredentialStatus.REVOKED) {
            return CredentialVerifyResult.fail("凭证已撤销");
        }
        
        List<String> accessGates = JsonUtils.fromJson(credential.getAccessGates(), List.class);
        if (!accessGates.contains(gateCode)) {
            return CredentialVerifyResult.fail("无此门禁通行权限");
        }
        
        credential.setUseCount(credential.getUseCount() + 1);
        credential.setLastUseTime(LocalDateTime.now());
        if (credential.getFirstUseTime() == null) {
            credential.setFirstUseTime(LocalDateTime.now());
        }
        credentialRepository.save(credential);
        
        return CredentialVerifyResult.success(credential);
    }
}
```

---

## 九、操作日志审计

### 9.1 操作日志设计

```java
@Entity
@Table(name = "auth_operation_log")
@Data
public class OperationLog {
    @Id
    private UUID id;
    private UUID projectId;
    
    private UUID userId;
    private String username;
    private String realName;
    
    private String module;
    private String operation;
    private String description;
    
    private String requestMethod;
    private String requestUrl;
    private String requestParams;
    private String requestBody;
    
    private Integer responseStatus;
    private String responseBody;
    
    private Long executionTime;
    private String ipAddress;
    private String userAgent;
    
    private Boolean hasError;
    private String errorMessage;
    
    private LocalDateTime createdAt;
}
```

### 9.2 日志记录切面

```java
@Aspect
@Component
@Slf4j
public class OperationLogAspect {
    
    @Autowired
    private OperationLogRepository operationLogRepository;
    
    @Around("@annotation(operationLog)")
    public Object around(ProceedingJoinPoint point, LogOperation operationLog) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        
        UUID userId = SecurityUtils.getCurrentUserId();
        String username = SecurityUtils.getCurrentUsername();
        
        OperationLog logEntry = new OperationLog();
        logEntry.setId(UUID.randomUUID());
        logEntry.setProjectId(ProjectContextHolder.getCurrentProjectId());
        logEntry.setUserId(userId);
        logEntry.setUsername(username);
        logEntry.setModule(operationLog.module());
        logEntry.setOperation(operationLog.operation());
        logEntry.setDescription(operationLog.description());
        logEntry.setRequestMethod(request.getMethod());
        logEntry.setRequestUrl(request.getRequestURI());
        logEntry.setIpAddress(getClientIp(request));
        logEntry.setUserAgent(request.getHeader("User-Agent"));
        logEntry.setCreatedAt(LocalDateTime.now());
        
        long startTime = System.currentTimeMillis();
        
        try {
            Object result = point.proceed();
            
            logEntry.setResponseStatus(200);
            logEntry.setHasError(false);
            
            return result;
        } catch (Exception e) {
            logEntry.setHasError(true);
            logEntry.setErrorMessage(e.getMessage());
            throw e;
        } finally {
            logEntry.setExecutionTime(System.currentTimeMillis() - startTime);
            operationLogRepository.save(logEntry);
        }
    }
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
    String module();
    String operation();
    String description();
    String businessType() default "";
}
```

---

## 十、API 接口

### 10.1 认证接口

```java
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "认证管理")
public class AuthController {

    @PostMapping("/login")
    @Operation(summary = "用户登录")
    public Result<LoginResponse> login(@RequestBody @Valid LoginRequest request);

    @PostMapping("/logout")
    @Operation(summary = "用户登出")
    public Result<Void> logout();

    @PostMapping("/refresh-token")
    @Operation(summary = "刷新Token")
    public Result<TokenResponse> refreshToken(@RequestBody RefreshTokenRequest request);

    @GetMapping("/current-user")
    @Operation(summary = "获取当前用户信息")
    public Result<UserVO> getCurrentUser();

    @PostMapping("/change-password")
    @Operation(summary = "修改密码")
    public Result<Void> changePassword(@RequestBody @Valid ChangePasswordRequest request);
    
    @GetMapping("/user-permissions")
    @Operation(summary = "获取用户权限信息")
    public Result<UserPermissionsResponse> getUserPermissions();
}
```

### 10.2 用户管理接口

```java
@RestController
@RequestMapping("/api/v1/auth/users")
@Tag(name = "用户管理")
public class UserController {

    @PostMapping
    @Operation(summary = "创建用户")
    public Result<UserVO> create(@RequestBody @Valid UserCreateRequest request);

    @GetMapping("/{id}")
    @Operation(summary = "获取用户详情")
    public Result<UserVO> getById(@PathVariable UUID id);

    @GetMapping
    @Operation(summary = "分页查询用户")
    public Result<Page<UserVO>> page(UserQueryRequest request);

    @PutMapping("/{id}")
    @Operation(summary = "更新用户")
    public Result<UserVO> update(@PathVariable UUID id, @RequestBody @Valid UserUpdateRequest request);

    @DeleteMapping("/{id}")
    @Operation(summary = "删除用户")
    public Result<Void> delete(@PathVariable UUID id);

    @PostMapping("/{id}/roles")
    @Operation(summary = "分配角色")
    public Result<Void> assignRoles(@PathVariable UUID id, @RequestBody UserRoleAssignRequest request);

    @PostMapping("/{id}/status")
    @Operation(summary = "修改用户状态")
    public Result<Void> changeStatus(@PathVariable UUID id, @RequestBody UserStatusChangeRequest request);
}
```

### 10.3 角色权限接口

```java
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "角色权限管理")
public class RolePermissionController {

    @PostMapping("/roles")
    @Operation(summary = "创建角色")
    public Result<RoleVO> createRole(@RequestBody @Valid RoleCreateRequest request);

    @GetMapping("/roles")
    @Operation(summary = "查询角色列表")
    public Result<List<RoleVO>> listRoles();

    @PutMapping("/roles/{id}")
    @Operation(summary = "更新角色")
    public Result<RoleVO> updateRole(@PathVariable UUID id, @RequestBody @Valid RoleUpdateRequest request);

    @DeleteMapping("/roles/{id}")
    @Operation(summary = "删除角色")
    public Result<Void> deleteRole(@PathVariable UUID id);

    @PostMapping("/roles/{id}/permissions")
    @Operation(summary = "分配权限")
    public Result<Void> assignPermissions(@PathVariable UUID id, @RequestBody RolePermissionAssignRequest request);

    @GetMapping("/permissions")
    @Operation(summary = "查询权限列表")
    public Result<List<PermissionVO>> listPermissions();

    @GetMapping("/permissions/tree")
    @Operation(summary = "获取权限树")
    public Result<List<PermissionTreeVO>> getPermissionTree();

    @GetMapping("/users/{id}/permissions")
    @Operation(summary = "获取用户权限")
    public Result<List<PermissionVO>> getUserPermissions(@PathVariable UUID id);

    @GetMapping("/users/{id}/menus")
    @Operation(summary = "获取用户菜单")
    public Result<List<MenuVO>> getUserMenus(@PathVariable UUID id);
    
    @PostMapping("/check")
    @Operation(summary = "校验权限")
    public Result<Boolean> checkPermission(@RequestBody PermissionCheckRequest request);
}
```

---

## 十一、实现状态与差异

### 11.1 实现状态

| 功能模块 | 实现状态 | 备注 |
|---------|---------|------|
| User | 🟢 已实现 | 基础CRUD |
| Role | 🟢 已实现 | 基础CRUD |
| Permission | 🟢 已实现 | 基础CRUD |
| JWT认证 | 🟢 已实现 | Token生成/验证 |
| 项目上下文 | 🟢 已实现 | 项目隔离 |
| 访客凭证 | 🟢 已实现 | 二维码生成/验证 |
| 数据权限 | 🟡 部分实现 | 框架已搭建，需补充PROJECT级别 |
| 操作日志 | 🟢 已实现 | AOP切面记录 |
| 按钮级权限 | ⏳ 待实现 | 细粒度操作权限 |
| 状态驱动权限 | ⏳ 待实现 | 根据业务状态控制操作 |
| 字段脱敏 | ⏳ 待实现 | 敏感字段脱敏 |

### 11.2 改进计划

| 优先级 | 改进项 | 说明 |
|--------|--------|------|
| P0 | 按钮级权限控制 | 新增操作按钮权限编码 |
| P0 | 状态驱动权限 | 工单状态与操作联动 |
| P1 | 数据权限完善 | 补充PROJECT级别 |
| P1 | 扫码强制机制 | 巡检扫码策略配置 |
| P2 | 字段脱敏 | 敏感字段脱敏 |
| P2 | 角色业务属性 | 新增业务类型、终端类型字段 |

---

## 十二、数据库表结构

```sql
-- 用户表
CREATE TABLE auth_user (
    id UUID PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    password VARCHAR(100) NOT NULL,
    salt VARCHAR(20) NOT NULL,
    real_name VARCHAR(100),
    phone VARCHAR(20),
    email VARCHAR(100),
    avatar VARCHAR(255),
    status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
    last_login_time TIMESTAMP,
    last_login_ip VARCHAR(50),
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    created_by UUID,
    UNIQUE(username)
);

-- 角色表
CREATE TABLE auth_role (
    id UUID PRIMARY KEY,
    project_id UUID,                    -- NULL表示系统级角色
    name VARCHAR(100) NOT NULL,
    code VARCHAR(50) NOT NULL,
    description VARCHAR(500),
    type VARCHAR(20) NOT NULL DEFAULT 'CUSTOM',
    data_scope VARCHAR(20) DEFAULT 'SELF',
    business_type VARCHAR(30),          -- 新增：业务类型
    terminal_type VARCHAR(20),          -- 新增：终端类型
    is_frontline BOOLEAN DEFAULT FALSE, -- 新增：是否一线人员
    enabled BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(project_id, code)
);

-- 权限表
CREATE TABLE auth_permission (
    id UUID PRIMARY KEY,
    project_id UUID,                    -- NULL表示系统级权限
    name VARCHAR(100) NOT NULL,
    code VARCHAR(100) NOT NULL,         -- module:resource:action
    type VARCHAR(20) NOT NULL,
    module VARCHAR(50),                 -- 新增：模块标识
    resource VARCHAR(50),               -- 新增：资源类型
    action VARCHAR(30),                 -- 新增：操作类型
    path VARCHAR(255),
    component VARCHAR(255),
    icon VARCHAR(50),
    sort_order INTEGER DEFAULT 0,
    parent_id UUID,
    level INTEGER DEFAULT 0,
    api_method VARCHAR(10),
    api_path VARCHAR(255),
    enabled BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(code, project_id)
);

-- 用户角色关联表
CREATE TABLE auth_user_role (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL,
    role_id UUID NOT NULL,
    project_id UUID,
    is_default BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(user_id, role_id, project_id)
);

-- 角色权限关联表
CREATE TABLE auth_role_permission (
    id UUID PRIMARY KEY,
    role_id UUID NOT NULL,
    permission_id UUID NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(role_id, permission_id)
);

-- 项目表
CREATE TABLE auth_project (
    id UUID PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    code VARCHAR(50) NOT NULL,
    description VARCHAR(500),
    province VARCHAR(50),
    city VARCHAR(50),
    district VARCHAR(50),
    address VARCHAR(255),
    manager_id UUID,
    manager_name VARCHAR(100),
    manager_phone VARCHAR(20),
    status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
    start_date DATE,
    end_date DATE,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(code)
);

-- 访客凭证表
CREATE TABLE auth_visitor_credential (
    id UUID PRIMARY KEY,
    project_id UUID NOT NULL,
    visitor_name VARCHAR(100) NOT NULL,
    visitor_phone VARCHAR(20),
    visitor_id_card VARCHAR(18),
    credential_code VARCHAR(50) NOT NULL,
    qr_code TEXT NOT NULL,
    expire_time TIMESTAMP NOT NULL,
    space_node_id UUID,
    access_gates JSONB,
    status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
    first_use_time TIMESTAMP,
    last_use_time TIMESTAMP,
    use_count INTEGER DEFAULT 0,
    creator_id UUID,
    creator_name VARCHAR(100),
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    UNIQUE(credential_code)
);

-- 操作日志表
CREATE TABLE auth_operation_log (
    id UUID PRIMARY KEY,
    project_id UUID,
    user_id UUID,
    username VARCHAR(50),
    real_name VARCHAR(100),
    module VARCHAR(50) NOT NULL,
    operation VARCHAR(50) NOT NULL,
    description VARCHAR(255),
    request_method VARCHAR(10),
    request_url VARCHAR(255),
    request_params TEXT,
    request_body TEXT,
    response_status INTEGER,
    response_body TEXT,
    execution_time BIGINT,
    ip_address VARCHAR(50),
    user_agent TEXT,
    has_error BOOLEAN DEFAULT FALSE,
    error_message TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- 创建索引
CREATE INDEX idx_user_username ON auth_user(username);
CREATE INDEX idx_user_status ON auth_user(status);
CREATE INDEX idx_role_project ON auth_role(project_id);
CREATE INDEX idx_permission_parent ON auth_permission(parent_id);
CREATE INDEX idx_permission_module ON auth_permission(module);
CREATE INDEX idx_user_role_user ON auth_user_role(user_id);
CREATE INDEX idx_user_role_role ON auth_user_role(role_id);
CREATE INDEX idx_role_permission_role ON auth_role_permission(role_id);
CREATE INDEX idx_credential_project ON auth_visitor_credential(project_id);
CREATE INDEX idx_credential_status ON auth_visitor_credential(status);
CREATE INDEX idx_operation_log_user ON auth_operation_log(user_id);
CREATE INDEX idx_operation_log_created ON auth_operation_log(created_at);
```

---

## 十三、更新记录

| 日期 | 更新内容 | 更新人 |
|------|----------|--------|
| 2026-02-10 | 创建权限与账户领域技术方案 | - |
| 2026-02-27 | 重构角色体系，新增13个角色 | - |
| 2026-02-27 | 新增权限编码规范 | - |
| 2026-02-27 | 新增数据权限四级范围 | - |
| 2026-02-27 | 新增状态驱动权限设计 | - |
| 2026-02-27 | 新增角色业务属性字段 | - |

---

**文档维护**: 本领域技术方案由 ether-auth 服务负责人维护
