# 权限与账户领域技术方案 **领域编号**: 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 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 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 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 permissions = permissionRepository .findPermissionsByUserIdAndProjectId(userId, projectId); return permissions.stream() .anyMatch(p -> p.getCode().equals(permissionCode)); } // 获取用户菜单 public List getUserMenus(UUID userId, UUID projectId) { List 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 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 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 login(@RequestBody @Valid LoginRequest request); @PostMapping("/logout") @Operation(summary = "用户登出") public Result logout(); @PostMapping("/refresh-token") @Operation(summary = "刷新Token") public Result refreshToken(@RequestBody RefreshTokenRequest request); @GetMapping("/current-user") @Operation(summary = "获取当前用户信息") public Result getCurrentUser(); @PostMapping("/change-password") @Operation(summary = "修改密码") public Result changePassword(@RequestBody @Valid ChangePasswordRequest request); @GetMapping("/user-permissions") @Operation(summary = "获取用户权限信息") public Result getUserPermissions(); } ``` ### 10.2 用户管理接口 ```java @RestController @RequestMapping("/api/v1/auth/users") @Tag(name = "用户管理") public class UserController { @PostMapping @Operation(summary = "创建用户") public Result create(@RequestBody @Valid UserCreateRequest request); @GetMapping("/{id}") @Operation(summary = "获取用户详情") public Result getById(@PathVariable UUID id); @GetMapping @Operation(summary = "分页查询用户") public Result> page(UserQueryRequest request); @PutMapping("/{id}") @Operation(summary = "更新用户") public Result update(@PathVariable UUID id, @RequestBody @Valid UserUpdateRequest request); @DeleteMapping("/{id}") @Operation(summary = "删除用户") public Result delete(@PathVariable UUID id); @PostMapping("/{id}/roles") @Operation(summary = "分配角色") public Result assignRoles(@PathVariable UUID id, @RequestBody UserRoleAssignRequest request); @PostMapping("/{id}/status") @Operation(summary = "修改用户状态") public Result 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 createRole(@RequestBody @Valid RoleCreateRequest request); @GetMapping("/roles") @Operation(summary = "查询角色列表") public Result> listRoles(); @PutMapping("/roles/{id}") @Operation(summary = "更新角色") public Result updateRole(@PathVariable UUID id, @RequestBody @Valid RoleUpdateRequest request); @DeleteMapping("/roles/{id}") @Operation(summary = "删除角色") public Result deleteRole(@PathVariable UUID id); @PostMapping("/roles/{id}/permissions") @Operation(summary = "分配权限") public Result assignPermissions(@PathVariable UUID id, @RequestBody RolePermissionAssignRequest request); @GetMapping("/permissions") @Operation(summary = "查询权限列表") public Result> listPermissions(); @GetMapping("/permissions/tree") @Operation(summary = "获取权限树") public Result> getPermissionTree(); @GetMapping("/users/{id}/permissions") @Operation(summary = "获取用户权限") public Result> getUserPermissions(@PathVariable UUID id); @GetMapping("/users/{id}/menus") @Operation(summary = "获取用户菜单") public Result> getUserMenus(@PathVariable UUID id); @PostMapping("/check") @Operation(summary = "校验权限") public Result 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 服务负责人维护