# 权限与账户领域技术方案 **领域编号**: 4.5 **微服务**: ether-auth **最后更新**: 2026-04-26 > **更新记录**: > - 2026-04-26: 反向同步实际代码实现到文档。主要变更:Role.type 改为 SYSTEM/PROJECT/DEPARTMENT 三级分类;Role.status 改为 RoleStatus 枚举;Permission 结构简化(parentCode 替代 parentId);UserRole 简化为 M2M 中间表;OperationLog 更名为 AuditLog(表名和字段结构变更);新增实体(EnterpriseUser/ProjectStaff/ProjectStaffRole/Resident/ResidentSpace/Space/UserProject/DataAccess/Dept/SysConfig);新增 API(部门管理/项目成员管理/数据访问授权/审计日志/系统配置/企业员工/用户项目关联);新增业务规则(登录失败锁定/密码强度校验/审计日志增强/项目成员角色/部门类型/住户认证);角色体系改为数据库动态管理;API路径去掉v1版本号。标注 TODO 项:Permission 需增加菜单路由属性(path/component/icon)。 --- ## 一、领域概述 ### 1.1 领域职责 权限与账户领域负责 Ether 平台的身份认证与授权管理: - 用户认证与登录 - RBAC 权限模型(角色-权限-用户关联) - 项目隔离与数据权限 - 按钮级权限控制 - 状态驱动权限 - 访客凭证管理 - 操作日志审计 ### 1.2 核心概念 | 概念 | 说明 | 对应实体 | |------|------|----------| | **用户** | 系统登录账户,支持四种用户类型 | User | | **企业员工** | 企业类型用户的扩展信息 | EnterpriseUser | | **项目员工** | 项目类型员工的扩展信息,含班次、岗位状态 | ProjectStaff | | **住户** | 业主/家属/租户,含认证流程 | Resident | | **角色** | 权限集合,支持系统级/项目级/部门级 | Role | | **权限** | 功能访问控制点,支持菜单/按钮/API三种类型 | Permission | | **部门** | 组织架构节点,支持树形结构 | Dept | | **用户-项目关联** | 用户参与项目的多对多关系 | UserProject | | **项目员工角色** | 项目员工在项目中的角色分配 | ProjectStaffRole | | **数据访问授权** | 细粒度数据访问控制记录(接口预留) | DataAccess | | **审计日志** | 操作审计记录 | AuditLog | | **房屋空间** | 项目下的房屋/空间信息 | Space | | **住户-房屋关联** | 住户与房屋的绑定关系 | ResidentSpace | | **系统配置** | 键值对形式的系统参数 | SysConfig | --- ## 二、角色体系设计 ### 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; // BCrypt加密密码 private String salt; // 密码盐值(BCrypt模式下冗余) private String realName; // 真实姓名 private String phone; private String email; private String avatar; // 头像URL private UserStatus status; // ACTIVE/LOCKED/DISABLED private UserType userType; // ENTERPRISE/PROJECT_STAFF/RESIDENT/CUSTOMER private UUID deptId; // 所属部门ID 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("禁用"); } public enum UserType { ENTERPRISE("企业员工"), PROJECT_STAFF("项目员工"), RESIDENT("住户"), CUSTOMER("客户"); } ``` #### 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(系统级)/PROJECT(项目级)/DEPARTMENT(部门级) // 数据权限范围(四级) private DataScope dataScope; // ALL/PROJECT/DEPARTMENT/SELF // 角色状态(枚举替代布尔值) private RoleStatus status; // ENABLED(启用)/DISABLED(禁用) @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "auth_role_permission", joinColumns = @JoinColumn(name = "role_id"), inverseJoinColumns = @JoinColumn(name = "permission_id") ) private List permissions; private LocalDateTime createdAt; private LocalDateTime updatedAt; } public enum RoleType { SYSTEM("系统级"), PROJECT("项目级"), DEPARTMENT("部门级"); } public enum RoleStatus { ENABLED("启用"), DISABLED("禁用"); } // 数据权限范围枚举(四级) public enum DataScope { ALL("全部数据"), PROJECT("本项目数据"), DEPARTMENT("本部门数据"), SELF("仅本人数据"); } ``` > **注意**: 实际代码中角色通过数据库动态管理,无硬编码预定义角色列表。原设计中的 businessType/terminalType/isFrontline 业务属性字段未实现,如需使用需后续补充。 #### Permission(权限) ```java @Entity @Table(name = "auth_permission") @Data public class Permission { @Id private UUID id; private String name; // 权限名称 private String code; // 权限编码: module:resource:action private String type; // 权限类型: MENU/BUTTON/API(String类型) // 资源信息 private String resource; // 资源路径(API类型) private String method; // HTTP方法(API类型) private String description; // 权限描述 // 树形结构(简化:parentCode替代parentId) private String parentCode; // 父权限代码(替代原 parentId UUID引用) private Integer sortOrder; // 排序序号 @ManyToMany(fetch = FetchType.LAZY, mappedBy = "permissions") private List roles; private LocalDateTime createdAt; private LocalDateTime updatedAt; } ``` > **TODO**: Permission 当前实现缺少菜单路由属性(path/component/icon),以及模块/资源/操作拆分字段(module/resource/action),需后续补充以支持前端菜单渲染和权限树展示。 ### 3.2 关联实体 #### UserRole(用户角色关联) > **注意**: 实际代码中 UserRole 已简化为 JPA 自动管理的 M2M 中间表 `auth_user_role`,仅包含 user_id 和 role_id 两个外键列,去掉了原设计中的 projectId 和 isDefault 字段。 ```java // auth_user_role 关联表(JPA自动管理,无独立实体) // 字段: user_id (FK → auth_user), role_id (FK → auth_role) ``` #### 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 审计日志设计 > **注意**: 实际代码中操作日志已更名为审计日志(AuditLog),表名从 `auth_operation_log` 改为 `sys_audit_log`,字段结构也有较大变更,增加了目标追踪(targetType/targetId)、操作内容(content)、操作结果(result)、租户ID(tenantId)等字段,并采用异步持久化。 ```java @Entity @Table(name = "sys_audit_log") @Data public class AuditLog { @Id private UUID id; private UUID userId; private String username; // 操作用户名 private String operation; // 操作描述 private String module; // 模块标识 private ActionType action; // 操作类型枚举 // 目标追踪(新增) private String targetType; // 目标类型 private String targetId; // 目标ID // 操作详情(替代原 requestBody/responseBody) private String content; // 操作内容 private String params; // 请求参数 private String result; // 操作结果 private String ipAddress; private String userAgent; private String requestUrl; private String requestMethod; private Integer executionTimeMs; // 执行耗时(ms) private AuditStatus status; // SUCCESS/FAIL private String errorMsg; // 错误信息 private UUID tenantId; // 租户ID(新增) private LocalDateTime createdAt; } public enum ActionType { CREATE, UPDATE, DELETE, QUERY, VIEW, LOGIN, LOGOUT, EXPORT, IMPORT, ASSIGN, REVOKE; } public enum AuditStatus { SUCCESS("成功"), FAIL("失败"); } ``` ### 9.2 日志记录注解 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OperationLog { String operation(); String module(); AuditLog.ActionType action(); } ``` > **业务规则**: > - 审计日志通过 `@OperationLog` 注解 + AOP 切面自动记录 > - 采用异步持久化(`auditLogExecutor` 线程池),避免影响业务性能 > - 查询窗口强制限制30天,超过30天的查询自动截断起始时间 > - 归档策略:90天以上日志归档(当前实现为直接删除,TODO: 导出至对象存储) > - 记录模块:USER / ROLE / PERMISSION / PROJECT / AUTH / DEPT / PROJECT_MEMBER / SYSTEM --- ## 十、API 接口 > **注意**: 实际代码中 API 路径已去掉 v1 版本号,统一为 `/api/auth` 前缀。 ### 10.1 认证接口 ```java @RestController @RequestMapping("/api/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") @Operation(summary = "刷新Token") public Result refreshToken(); @GetMapping("/me") @Operation(summary = "获取当前用户信息") public Result getCurrentUser(); } ``` > **登录流程增强**(实际代码新增): > 1. 检查登录失败锁定(Redis,5次失败锁定10分钟) > 2. 查询用户(含角色) > 3. 验证密码(BCrypt) > 4. 检查用户状态(LOCKED/DISABLED拒绝) > 5. 收集所有角色(用户直接角色 + 项目员工角色) > 6. 生成JWT Token(含userId、username、roles claims) > 7. 返回Token和用户基本信息 ### 10.2 用户管理接口 ```java @RestController @RequestMapping("/api/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); @PutMapping("/{id}/password") @Operation(summary = "修改密码") public Result changePassword(@PathVariable UUID id, @RequestBody @Valid ChangePasswordRequest request); @PostMapping("/{id}/roles") @Operation(summary = "分配角色") public Result assignRoles(@PathVariable UUID id, @RequestBody UserRoleAssignRequest request); @GetMapping("/{id}/projects") @Operation(summary = "获取用户项目列表") public Result> getUserProjects(@PathVariable UUID id); @PostMapping("/{id}/projects") @Operation(summary = "添加用户到项目") public Result addUserToProject(@PathVariable UUID id, @RequestBody UserProjectRequest request); @DeleteMapping("/{id}/projects/{projectId}") @Operation(summary = "从项目移除用户") public Result removeUserFromProject(@PathVariable UUID id, @PathVariable UUID projectId); @GetMapping("/enterprise") @Operation(summary = "获取企业员工列表") public Result> getEnterpriseUsers(); } ``` > **密码修改规则**(实际代码新增): > - 需验证原密码 > - 新密码需通过强度校验(8-20位,大小写+数字+特殊字符) > - 弱密码检测(黑名单:password、123456、admin等) ### 10.3 角色权限接口 ```java @RestController @RequestMapping("/api/auth/roles") @Tag(name = "角色管理") public class RoleController { @PostMapping @Operation(summary = "创建角色") public Result createRole(@RequestBody @Valid RoleCreateRequest request); @GetMapping @Operation(summary = "分页查询角色列表") public Result> listRoles(); @GetMapping("/{id}") @Operation(summary = "获取角色详情") public Result getRoleById(@PathVariable UUID id); @GetMapping("/project/{projectId}") @Operation(summary = "根据项目ID查询角色") public Result> getRolesByProject(@PathVariable UUID projectId); @PutMapping("/{id}") @Operation(summary = "更新角色") public Result updateRole(@PathVariable UUID id, @RequestBody @Valid RoleUpdateRequest request); @DeleteMapping("/{id}") @Operation(summary = "删除角色") public Result deleteRole(@PathVariable UUID id); @PostMapping("/{id}/permissions") @Operation(summary = "分配权限") public Result assignPermissions(@PathVariable UUID id, @RequestBody RolePermissionAssignRequest request); @GetMapping("/{id}/permissions") @Operation(summary = "获取角色权限列表") public Result> getRolePermissions(@PathVariable UUID id); @GetMapping("/{id}/users") @Operation(summary = "获取拥有某角色的用户") public Result> getRoleUsers(@PathVariable UUID id); } ``` ### 10.4 权限管理接口 ```java @RestController @RequestMapping("/api/auth/permissions") @Tag(name = "权限管理") public class PermissionController { @PostMapping @Operation(summary = "创建权限") public Result createPermission(@RequestBody @Valid PermissionCreateRequest request); @GetMapping @Operation(summary = "分页查询权限列表") public Result> listPermissions(); @GetMapping("/{id}") @Operation(summary = "获取权限详情") public Result getPermissionById(@PathVariable UUID id); @GetMapping("/type/{type}") @Operation(summary = "根据类型查询权限") public Result> getPermissionsByType(@PathVariable String type); @GetMapping("/menus") @Operation(summary = "查询所有菜单权限") public Result> listMenuPermissions(); @PutMapping("/{id}") @Operation(summary = "更新权限") public Result updatePermission(@PathVariable UUID id, @RequestBody @Valid PermissionUpdateRequest request); @DeleteMapping("/{id}") @Operation(summary = "删除权限") public Result deletePermission(@PathVariable UUID id); } ``` ### 10.5 部门管理接口(新增) ```java @RestController @RequestMapping("/api/auth/depts") @Tag(name = "部门管理") public class DeptController { @GetMapping("/tree") @Operation(summary = "获取部门树") public Result> getDeptTree(); @GetMapping @Operation(summary = "获取所有启用部门") public Result> listActiveDepts(); @GetMapping("/{id}") @Operation(summary = "获取部门详情") public Result getById(@PathVariable UUID id); @PostMapping @Operation(summary = "创建部门") public Result create(@RequestBody @Valid DeptCreateRequest request); @PutMapping("/{id}") @Operation(summary = "更新部门") public Result update(@PathVariable UUID id, @RequestBody @Valid DeptUpdateRequest request); @DeleteMapping("/{id}") @Operation(summary = "删除部门") public Result delete(@PathVariable UUID id); @GetMapping("/{deptId}/members") @Operation(summary = "获取部门成员") public Result> getDeptMembers(@PathVariable UUID deptId); @GetMapping("/by-type/{deptType}") @Operation(summary = "根据类型查询部门") public Result> getDeptsByType(@PathVariable String deptType); } ``` > **部门类型**: ADMIN(行政管理) / ENGINEERING(工程部) / SECURITY(安保部) / CS(客服部) / CLEANING(保洁部) ### 10.6 项目成员管理接口(新增) ```java @RestController @RequestMapping("/api/auth/projects") @Tag(name = "项目成员管理") public class ProjectMemberController { @GetMapping("/{projectId}/members") @Operation(summary = "查询项目成员列表(分页)") public Result> listMembers(@PathVariable UUID projectId); @GetMapping("/{projectId}/available-members") @Operation(summary = "获取可添加成员(企业员工)") public Result> getAvailableMembers(@PathVariable UUID projectId); @PostMapping("/{projectId}/members") @Operation(summary = "添加项目成员") public Result addMember(@PathVariable UUID projectId, @RequestBody AddMemberRequest request); @DeleteMapping("/{projectId}/members/{userId}") @Operation(summary = "移除项目成员") public Result removeMember(@PathVariable UUID projectId, @PathVariable UUID userId); } ``` ### 10.7 数据访问授权接口(新增,接口预留) ```java @RestController @RequestMapping("/api/data-access") @Tag(name = "数据访问授权") public class DataAccessController { @PostMapping @Operation(summary = "授予数据访问权限") public Result grantAccess(@RequestBody @Valid DataAccessGrantRequest request); @DeleteMapping("/{id}") @Operation(summary = "撤销数据访问权限") public Result revokeAccess(@PathVariable UUID id); @GetMapping @Operation(summary = "查询数据访问记录") public Result> queryAccess(@RequestParam String dataType, @RequestParam UUID dataId); } ``` ### 10.8 审计日志接口(新增) ```java @RestController @RequestMapping("/api/audit-logs") @Tag(name = "审计日志") public class AuditLogController { @GetMapping @Operation(summary = "分页查询审计日志") public Result> page(AuditLogQueryRequest request); @GetMapping("/modules") @Operation(summary = "获取模块列表") public Result> getModules(); @GetMapping("/actions") @Operation(summary = "获取操作类型列表") public Result> getActions(); @GetMapping("/stats") @Operation(summary = "获取最近30天日志统计") public Result getStats(); } ``` > **查询约束**: 强制限制查询范围不超过30天,防止大量数据查询。 ### 10.9 系统配置接口(新增) ```java @RestController @RequestMapping("/api/config") @Tag(name = "系统配置") public class SysConfigController { @GetMapping @Operation(summary = "获取所有配置项") public Result> listConfigs(); @GetMapping("/{configKey}") @Operation(summary = "根据键获取配置") public Result getConfig(@PathVariable String configKey); @PutMapping("/{configKey}") @Operation(summary = "更新单个配置") public Result updateConfig(@PathVariable String configKey, @RequestBody String configValue); @PutMapping @Operation(summary = "批量更新配置") public Result batchUpdateConfigs(@RequestBody List requests); } ``` --- ## 十一、实现状态与差异 ### 11.1 实现状态 | 功能模块 | 实现状态 | 备注 | |---------|---------|------| | User | 🟢 已实现 | 基础CRUD,含 userType/deptId 扩展字段 | | Role | 🟢 已实现 | 基础CRUD,三级分类(SYSTEM/PROJECT/DEPARTMENT),RoleStatus枚举 | | Permission | 🟢 已实现 | 基础CRUD,简化结构(parentCode替代parentId) | | JWT认证 | 🟢 已实现 | Token生成/验证,含登录失败锁定(Redis) | | 项目上下文 | 🟢 已实现 | 项目隔离 | | 审计日志 | 🟢 已实现 | AuditLog(原OperationLog),异步持久化,30天查询窗口 | | 部门管理 | 🟢 已实现 | 树形结构,5种部门类型 | | 项目成员管理 | 🟢 已实现 | ProjectStaff + ProjectStaffRole | | 企业员工 | 🟢 已实现 | EnterpriseUser扩展实体 | | 住户管理 | 🟢 已实现 | Resident + ResidentSpace + Space | | 用户项目关联 | 🟢 已实现 | UserProject多对多 | | 系统配置 | 🟢 已实现 | SysConfig键值对 | | 数据访问授权 | 🟢 已实现 | DataAccess接口预留 | | 登录失败锁定 | 🟢 已实现 | Redis实现,5次失败锁10分钟 | | 密码强度校验 | 🟢 已实现 | 8-20位+大小写数字特殊字符+弱密码检测 | | 数据权限 | 🟡 部分实现 | DataScopeService提供判断方法,未实现SQL自动注入 | | 按钮级权限 | ⏳ 待实现 | 细粒度操作权限 | | 状态驱动权限 | ⏳ 待实现 | 根据业务状态控制操作 | | 字段脱敏 | ⏳ 待实现 | 敏感字段脱敏 | | 访客凭证 | 🔴 未实现 | 原设计有但未在认证模块实现 | | 角色业务属性 | 🔴 未实现 | businessType/terminalType/isFrontline未实现 | | 权限树端点 | 🔴 未实现 | GET /permissions/tree | | 权限校验端点 | 🔴 未实现 | POST /check | | 用户菜单端点 | 🔴 未实现 | GET /users/{id}/menus | | 用户权限查询端点 | 🔴 未实现 | GET /users/{id}/permissions | | Permission菜单路由属性 | 🔴 未实现 | path/component/icon字段缺失 | ### 11.2 改进计划 | 优先级 | 改进项 | 说明 | |--------|--------|------| | P0 | Permission菜单路由属性 | 增加 path/component/icon/module/action 字段,支持前端菜单渲染 | | P0 | 权限树端点 | 实现 GET /permissions/tree | | P0 | 用户菜单端点 | 实现 GET /users/{id}/menus | | P0 | 权限校验端点 | 实现 POST /check | | P1 | 数据权限完善 | 实现SQL自动注入,补充PROJECT级别 | | P1 | 角色业务属性 | 新增 businessType/terminalType/isFrontline 字段 | | P1 | 按钮级权限控制 | 新增操作按钮权限编码 | | P1 | 状态驱动权限 | 工单状态与操作联动 | | P2 | 字段脱敏 | 敏感字段脱敏 | | 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 | 新增角色业务属性字段 | - | | 2026-04-26 | 反向同步实际代码实现:Role.type改为SYSTEM/PROJECT/DEPARTMENT三级分类;Role.status改为RoleStatus枚举;Permission结构简化(parentCode替代parentId);UserRole简化为M2M中间表;OperationLog更名为AuditLog;新增实体(EnterpriseUser/ProjectStaff/ProjectStaffRole/Resident/ResidentSpace/Space/UserProject/DataAccess/Dept/SysConfig);新增API(部门管理/项目成员管理/数据访问授权/审计日志/系统配置/企业员工/用户项目关联);新增业务规则(登录失败锁定/密码强度校验/审计日志增强/项目成员角色/部门类型/住户认证);角色体系改为数据库动态管理;API路径去掉v1版本号;标注TODO: Permission需增加菜单路由属性 | - | --- **文档维护**: 本领域技术方案由 ether-auth 服务负责人维护