ether-docs/_archive/domains-old/05-AUTH.md

1416 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 权限与账户领域技术方案
**领域编号**: 4.5
**微服务**: ether-auth
**最后更新**: 2026-04-26
> **更新记录**:
> - 2026-04-26: 反向同步实际代码实现到文档。主要变更Role.type 改为 SYSTEM/PROJECT/DEPARTMENT 三级分类Role.status 改为 RoleStatus 枚举Permission 结构简化parentCode 替代 parentIdUserRole 简化为 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<Role> 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<Permission> 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/APIString类型
// 资源信息
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<Role> 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<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 审计日志设计
> **注意**: 实际代码中操作日志已更名为审计日志AuditLog表名从 `auth_operation_log` 改为 `sys_audit_log`字段结构也有较大变更增加了目标追踪targetType/targetId、操作内容content、操作结果result、租户IDtenantId等字段并采用异步持久化。
```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<LoginResponse> login(@RequestBody @Valid LoginRequest request);
@PostMapping("/logout")
@Operation(summary = "用户登出")
public Result<Void> logout();
@PostMapping("/refresh")
@Operation(summary = "刷新Token")
public Result<TokenResponse> refreshToken();
@GetMapping("/me")
@Operation(summary = "获取当前用户信息")
public Result<UserVO> getCurrentUser();
}
```
> **登录流程增强**(实际代码新增):
> 1. 检查登录失败锁定Redis5次失败锁定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<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);
@PutMapping("/{id}/password")
@Operation(summary = "修改密码")
public Result<Void> changePassword(@PathVariable UUID id, @RequestBody @Valid ChangePasswordRequest request);
@PostMapping("/{id}/roles")
@Operation(summary = "分配角色")
public Result<Void> assignRoles(@PathVariable UUID id, @RequestBody UserRoleAssignRequest request);
@GetMapping("/{id}/projects")
@Operation(summary = "获取用户项目列表")
public Result<List<UserProjectVO>> getUserProjects(@PathVariable UUID id);
@PostMapping("/{id}/projects")
@Operation(summary = "添加用户到项目")
public Result<Void> addUserToProject(@PathVariable UUID id, @RequestBody UserProjectRequest request);
@DeleteMapping("/{id}/projects/{projectId}")
@Operation(summary = "从项目移除用户")
public Result<Void> removeUserFromProject(@PathVariable UUID id, @PathVariable UUID projectId);
@GetMapping("/enterprise")
@Operation(summary = "获取企业员工列表")
public Result<List<EnterpriseUserVO>> getEnterpriseUsers();
}
```
> **密码修改规则**(实际代码新增):
> - 需验证原密码
> - 新密码需通过强度校验8-20位大小写+数字+特殊字符)
> - 弱密码检测黑名单password、123456、admin等
### 10.3 角色权限接口
```java
@RestController
@RequestMapping("/api/auth/roles")
@Tag(name = "角色管理")
public class RoleController {
@PostMapping
@Operation(summary = "创建角色")
public Result<RoleVO> createRole(@RequestBody @Valid RoleCreateRequest request);
@GetMapping
@Operation(summary = "分页查询角色列表")
public Result<Page<RoleVO>> listRoles();
@GetMapping("/{id}")
@Operation(summary = "获取角色详情")
public Result<RoleVO> getRoleById(@PathVariable UUID id);
@GetMapping("/project/{projectId}")
@Operation(summary = "根据项目ID查询角色")
public Result<List<RoleVO>> getRolesByProject(@PathVariable UUID projectId);
@PutMapping("/{id}")
@Operation(summary = "更新角色")
public Result<RoleVO> updateRole(@PathVariable UUID id, @RequestBody @Valid RoleUpdateRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "删除角色")
public Result<Void> deleteRole(@PathVariable UUID id);
@PostMapping("/{id}/permissions")
@Operation(summary = "分配权限")
public Result<Void> assignPermissions(@PathVariable UUID id, @RequestBody RolePermissionAssignRequest request);
@GetMapping("/{id}/permissions")
@Operation(summary = "获取角色权限列表")
public Result<List<PermissionVO>> getRolePermissions(@PathVariable UUID id);
@GetMapping("/{id}/users")
@Operation(summary = "获取拥有某角色的用户")
public Result<List<UserVO>> getRoleUsers(@PathVariable UUID id);
}
```
### 10.4 权限管理接口
```java
@RestController
@RequestMapping("/api/auth/permissions")
@Tag(name = "权限管理")
public class PermissionController {
@PostMapping
@Operation(summary = "创建权限")
public Result<PermissionVO> createPermission(@RequestBody @Valid PermissionCreateRequest request);
@GetMapping
@Operation(summary = "分页查询权限列表")
public Result<Page<PermissionVO>> listPermissions();
@GetMapping("/{id}")
@Operation(summary = "获取权限详情")
public Result<PermissionVO> getPermissionById(@PathVariable UUID id);
@GetMapping("/type/{type}")
@Operation(summary = "根据类型查询权限")
public Result<List<PermissionVO>> getPermissionsByType(@PathVariable String type);
@GetMapping("/menus")
@Operation(summary = "查询所有菜单权限")
public Result<List<PermissionVO>> listMenuPermissions();
@PutMapping("/{id}")
@Operation(summary = "更新权限")
public Result<PermissionVO> updatePermission(@PathVariable UUID id, @RequestBody @Valid PermissionUpdateRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "删除权限")
public Result<Void> deletePermission(@PathVariable UUID id);
}
```
### 10.5 部门管理接口(新增)
```java
@RestController
@RequestMapping("/api/auth/depts")
@Tag(name = "部门管理")
public class DeptController {
@GetMapping("/tree")
@Operation(summary = "获取部门树")
public Result<List<DeptTreeVO>> getDeptTree();
@GetMapping
@Operation(summary = "获取所有启用部门")
public Result<List<DeptVO>> listActiveDepts();
@GetMapping("/{id}")
@Operation(summary = "获取部门详情")
public Result<DeptVO> getById(@PathVariable UUID id);
@PostMapping
@Operation(summary = "创建部门")
public Result<DeptVO> create(@RequestBody @Valid DeptCreateRequest request);
@PutMapping("/{id}")
@Operation(summary = "更新部门")
public Result<DeptVO> update(@PathVariable UUID id, @RequestBody @Valid DeptUpdateRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "删除部门")
public Result<Void> delete(@PathVariable UUID id);
@GetMapping("/{deptId}/members")
@Operation(summary = "获取部门成员")
public Result<List<UserVO>> getDeptMembers(@PathVariable UUID deptId);
@GetMapping("/by-type/{deptType}")
@Operation(summary = "根据类型查询部门")
public Result<List<DeptVO>> 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<Page<ProjectStaffVO>> listMembers(@PathVariable UUID projectId);
@GetMapping("/{projectId}/available-members")
@Operation(summary = "获取可添加成员(企业员工)")
public Result<List<EnterpriseUserVO>> getAvailableMembers(@PathVariable UUID projectId);
@PostMapping("/{projectId}/members")
@Operation(summary = "添加项目成员")
public Result<Void> addMember(@PathVariable UUID projectId, @RequestBody AddMemberRequest request);
@DeleteMapping("/{projectId}/members/{userId}")
@Operation(summary = "移除项目成员")
public Result<Void> 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<DataAccessVO> grantAccess(@RequestBody @Valid DataAccessGrantRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "撤销数据访问权限")
public Result<Void> revokeAccess(@PathVariable UUID id);
@GetMapping
@Operation(summary = "查询数据访问记录")
public Result<List<DataAccessVO>> 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<AuditLogVO>> page(AuditLogQueryRequest request);
@GetMapping("/modules")
@Operation(summary = "获取模块列表")
public Result<List<String>> getModules();
@GetMapping("/actions")
@Operation(summary = "获取操作类型列表")
public Result<List<String>> getActions();
@GetMapping("/stats")
@Operation(summary = "获取最近30天日志统计")
public Result<AuditStatsVO> getStats();
}
```
> **查询约束**: 强制限制查询范围不超过30天防止大量数据查询。
### 10.9 系统配置接口(新增)
```java
@RestController
@RequestMapping("/api/config")
@Tag(name = "系统配置")
public class SysConfigController {
@GetMapping
@Operation(summary = "获取所有配置项")
public Result<List<SysConfigVO>> listConfigs();
@GetMapping("/{configKey}")
@Operation(summary = "根据键获取配置")
public Result<SysConfigVO> getConfig(@PathVariable String configKey);
@PutMapping("/{configKey}")
@Operation(summary = "更新单个配置")
public Result<Void> updateConfig(@PathVariable String configKey, @RequestBody String configValue);
@PutMapping
@Operation(summary = "批量更新配置")
public Result<Void> batchUpdateConfigs(@RequestBody List<SysConfigUpdateRequest> 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 服务负责人维护