ether-docs/02-DESIGN/domains/05-AUTH.md

1234 lines
38 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-02-27
---
## 一、领域概述
### 1.1 领域职责
权限与账户领域负责 Ether 平台的身份认证与授权管理:
- 用户认证与登录
- RBAC 权限模型(角色-权限-用户关联)
- 项目隔离与数据权限
- 按钮级权限控制
- 状态驱动权限
- 访客凭证管理
- 操作日志审计
### 1.2 核心概念
| 概念 | 说明 | 对应实体 |
|------|------|----------|
| **用户** | 系统登录账户 | User |
| **角色** | 权限集合 | Role |
| **权限** | 功能访问控制点 | Permission |
| **项目** | 多租户隔离单位 | Project |
| **访客凭证** | 临时访问授权 | VisitorCredential |
---
## 二、角色体系设计
### 2.1 角色总览
| 类型 | 角色编码 | 角色名称 | 数据范围 | 终端类型 | 一线人员 |
|------|---------|---------|---------|---------|---------|
| **系统级** |
| 系统管理 | SUPER_ADMIN | 超级管理员 | ALL | ALL | 否 |
| 系统管理 | SYS_ADMIN | 系统管理员 | ALL | ADMIN_ONLY | 否 |
| **项目管理** |
| 项目管理 | PROPERTY_MANAGER | 物业经理 | PROJECT | ALL | 否 |
| 项目管理 | PROJECT_MANAGER | 项目经理 | PROJECT | ALL | 否 |
| **部门主管** |
| 工程管理 | ENGINEERING_LEAD | 工程主管 | DEPARTMENT | ALL | 否 |
| 安保管理 | SECURITY_LEAD | 安保主管 | DEPARTMENT | ALL | 否 |
| 保洁管理 | CLEANING_LEAD | 保洁主管 | DEPARTMENT | ALL | 否 |
| 财务管理 | FINANCE_LEAD | 财务主管 | DEPARTMENT | ALL | 否 |
| **一线执行** |
| 工程执行 | MAINTENANCE_STAFF | 维修人员 | SELF | MOBILE_ONLY | 是 |
| 安保执行 | SECURITY_STAFF | 安保人员 | SELF | MOBILE_ONLY | 是 |
| 保洁执行 | CLEANING_STAFF | 保洁人员 | SELF | MOBILE_ONLY | 是 |
| **服务支持** |
| 业主服务 | CS_STAFF | 客服人员 | PROJECT | ALL | 否 |
| **外部用户** |
| 业主 | OWNER | 业主 | SELF | OWNER_APP | 否 |
### 2.2 角色职责说明
#### 2.2.1 系统级角色
| 角色 | 职责范围 | 典型场景 |
|------|---------|---------|
| **超级管理员** | 系统最高权限,管理所有项目和用户 | 系统初始化、用户创建、项目创建 |
| **系统管理员** | 系统配置管理,不参与业务操作 | 系统参数配置、日志查看 |
#### 2.2.2 项目管理角色
| 角色 | 职责范围 | 典型场景 |
|------|---------|---------|
| **物业经理** | 项目经营第一负责人,拥有本项目全部权限 | 项目整体运营、费用审核、合同审批 |
| **项目经理** | 单项目管理,拥有本项目全部业务权限 | 工单管理、人员分配、报表查看 |
#### 2.2.3 部门主管角色
| 角色 | 管理模块 | 职责范围 | 典型场景 |
|------|---------|---------|---------|
| **工程主管** | 工单、设备、工程巡检 | 本部门工程事务管理 | 工单分配、设备管理、巡检计划制定 |
| **安保主管** | 安保巡检、访客管理 | 本部门安保事务管理 | 安保巡检计划、访客核验监督 |
| **保洁主管** | 保洁巡检、品质检查 | 本部门保洁事务管理 | 保洁巡检计划、品质检查 |
| **财务主管** | 账单、收费、费用审核 | 本部门财务事务管理 | 账单生成、费用审核、报表导出 |
#### 2.2.4 一线执行角色
| 角色 | 执行范围 | 终端限制 | 典型场景 |
|------|---------|---------|---------|
| **维修人员** | 工单处理、设备巡检 | 仅移动端 | 接单、维修、填报费用、设备巡检 |
| **安保人员** | 安保巡检、访客核验 | 仅移动端 | 扫码签到、访客核验、异常报送 |
| **保洁人员** | 保洁巡检、品质检查 | 仅移动端 | 扫码签到、品质异常报送 |
#### 2.2.5 服务支持角色
| 角色 | 职责范围 | 典型场景 |
|------|---------|---------|
| **客服人员** | 业主服务接口,不参与工单流程 | 业主咨询、资料维护、访客辅助核验 |
#### 2.2.6 外部用户角色
| 角色 | 权限范围 | 典型场景 |
|------|---------|---------|
| **业主** | 本人房产相关数据 | 在线报修、缴费、访客邀请、评价 |
---
## 三、领域模型
### 3.1 聚合根设计
#### User系统用户
```java
@Entity
@Table(name = "auth_user")
@Data
public class User {
@Id
private UUID id;
private String username; // 登录账号
private String password; // 加密密码
private String salt; // 密码盐值
private String realName; // 真实姓名
private String phone;
private String email;
private String avatar; // 头像URL
private UserStatus status; // ACTIVE/LOCKED/DISABLED
private LocalDateTime lastLoginTime;
private String lastLoginIp;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "auth_user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private List<Role> roles;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private UUID createdBy;
}
public enum UserStatus {
ACTIVE("正常"),
LOCKED("锁定"),
DISABLED("禁用");
}
```
#### Role角色
```java
@Entity
@Table(name = "auth_role")
@Data
public class Role {
@Id
private UUID id;
private UUID projectId; // 项目隔离NULL表示系统级
private String name; // 角色名称
private String code; // 角色编码
private String description; // 角色描述
private RoleType type; // SYSTEM(系统预设)/CUSTOM(自定义)
// 数据权限范围(四级)
private DataScope dataScope; // ALL/PROJECT/DEPARTMENT/SELF
// 新增:业务属性
private BusinessType businessType; // 业务类型
private TerminalType terminalType; // 终端类型限制
private Boolean isFrontline; // 是否一线执行人员
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "auth_role_permission",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private List<Permission> permissions;
private Boolean enabled;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
public enum RoleType {
SYSTEM("系统预设"),
CUSTOM("自定义");
}
// 数据权限范围枚举(四级)
public enum DataScope {
ALL("全部数据"),
PROJECT("本项目数据"),
DEPARTMENT("本部门数据"),
SELF("仅本人数据");
}
// 业务类型枚举
public enum BusinessType {
MANAGEMENT("管理类"),
ENGINEERING("工程类"),
SECURITY("安保类"),
CLEANING("保洁类"),
CUSTOMER_SERVICE("客服类"),
FINANCE("财务类");
}
// 终端类型枚举
public enum TerminalType {
ALL("全终端"),
ADMIN_ONLY("仅管理后台"),
MOBILE_ONLY("仅移动端"),
OWNER_APP("业主端");
}
```
#### Permission权限
```java
@Entity
@Table(name = "auth_permission")
@Data
public class Permission {
@Id
private UUID id;
private UUID projectId; // 项目隔离NULL表示系统级
private String name; // 权限名称
private String code; // 权限编码: module:resource:action
private PermissionType type; // MENU/BUTTON/API
// 模块和资源信息
private String module; // 模块标识: system/mdm/ops/finance
private String resource; // 资源类型: user/role/work_order
private String action; // 操作类型: view/create/edit/delete/assign
// 菜单属性
private String path; // 路由路径
private String component; // 组件路径
private String icon; // 图标
private Integer sortOrder; // 排序
// 树形结构
private UUID parentId; // 父权限ID
private Integer level; // 层级
// API属性
private String apiMethod; // GET/POST/PUT/DELETE
private String apiPath; // API路径
private Boolean enabled;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
public enum PermissionType {
MENU("菜单"),
BUTTON("按钮"),
API("接口");
}
```
### 3.2 关联实体
#### UserRole用户角色关联
```java
@Entity
@Table(name = "auth_user_role")
@Data
public class UserRole {
@Id
private UUID id;
private UUID userId;
private UUID roleId;
private UUID projectId; // 项目隔离
private Boolean isDefault; // 是否默认角色
private LocalDateTime createdAt;
}
```
#### RolePermission角色权限关联
```java
@Entity
@Table(name = "auth_role_permission")
@Data
public class RolePermission {
@Id
private UUID id;
private UUID roleId;
private UUID permissionId;
private LocalDateTime createdAt;
}
```
---
## 四、权限编码规范
### 4.1 编码格式
**格式**: `module:resource:action`
| 组成部分 | 说明 | 示例值 |
|---------|------|--------|
| module | 模块标识 | system, mdm, ops, asset, finance |
| resource | 资源类型 | user, role, work_order, equipment, bill |
| action | 操作类型 | view, create, edit, delete, assign, accept, complete |
### 4.2 操作类型说明
| 操作类型 | 说明 | 适用场景 |
|---------|------|---------|
| view | 查看 | 列表、详情查看 |
| create | 创建 | 新增记录 |
| edit | 编辑 | 修改记录 |
| delete | 删除 | 删除记录 |
| assign | 分配 | 工单分配、任务分配 |
| accept | 接单 | 工单接单 |
| start | 开始 | 开始处理、开始巡检 |
| complete | 完成 | 完成工单、完成巡检 |
| transfer | 转单 | 工单转单 |
| close | 关闭 | 关闭工单 |
| evaluate | 评价 | 工单评价 |
| report_fee | 填报费用 | 工单费用填报 |
| audit_fee | 费用审核 | 费用审核 |
| audit_quality | 质量审核 | 质量审核 |
| export | 导出 | 数据导出 |
| generate_qr | 生成二维码 | 二维码生成 |
| scan_view | 扫码查看 | 扫码查看设备 |
| force_close | 强制闭环 | 逾期任务强制关闭 |
| verify | 核验 | 访客核验 |
---
## 五、认证授权流程
### 5.1 JWT Token 认证
```java
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration:86400000}")
private long jwtExpiration;
private SecretKey secretKey;
@PostConstruct
public void init() {
this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(User user, UUID projectId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(user.getId().toString())
.claim("username", user.getUsername())
.claim("realName", user.getRealName())
.claim("projectId", projectId != null ? projectId.toString() : null)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public UUID getUserIdFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
return UUID.fromString(claims.getSubject());
}
public UUID getProjectIdFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
String projectId = claims.get("projectId", String.class);
return projectId != null ? UUID.fromString(projectId) : null;
}
}
```
### 5.2 项目上下文
```java
public class ProjectContextHolder {
private static final ThreadLocal<UUID> PROJECT_CONTEXT = new ThreadLocal<>();
public static void setCurrentProjectId(UUID projectId) {
PROJECT_CONTEXT.set(projectId);
}
public static UUID getCurrentProjectId() {
return PROJECT_CONTEXT.get();
}
public static void clear() {
PROJECT_CONTEXT.remove();
}
}
@Component
public class ProjectContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String projectId = request.getHeader("X-Project-ID");
if (StringUtils.hasText(projectId)) {
try {
ProjectContextHolder.setCurrentProjectId(UUID.fromString(projectId));
} catch (IllegalArgumentException e) {
// 无效的Project ID格式
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ProjectContextHolder.clear();
}
}
```
### 5.3 权限检查
```java
@Service
public class PermissionService {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
// 检查用户是否有指定权限
public boolean hasPermission(UUID userId, String permissionCode) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("用户不存在"));
// 超级管理员拥有所有权限
if (isSuperAdmin(user)) {
return true;
}
// 检查用户角色是否包含该权限
for (Role role : user.getRoles()) {
if (!role.getEnabled()) {
continue;
}
for (Permission permission : role.getPermissions()) {
if (permission.getCode().equals(permissionCode)) {
return true;
}
}
}
return false;
}
// 检查用户是否有指定权限(带项目隔离)
public boolean hasPermission(UUID userId, UUID projectId, String permissionCode) {
// 超级管理员
if (isSuperAdmin(userId)) {
return true;
}
// 获取用户在该项目下的权限
List<Permission> permissions = permissionRepository
.findPermissionsByUserIdAndProjectId(userId, projectId);
return permissions.stream()
.anyMatch(p -> p.getCode().equals(permissionCode));
}
// 获取用户菜单
public List<Permission> getUserMenus(UUID userId, UUID projectId) {
List<Permission> permissions = permissionRepository
.findByUserIdAndProjectId(userId, projectId);
return permissions.stream()
.filter(p -> p.getType() == PermissionType.MENU)
.filter(Permission::getEnabled)
.sorted(Comparator.comparing(Permission::getSortOrder))
.collect(Collectors.toList());
}
}
```
---
## 六、数据权限设计
### 6.1 数据范围级别
| 数据范围 | 编码 | 说明 | SQL预设 |
|---------|------|------|---------|
| 全部 | ALL | 所有项目数据 | `WHERE 1=1` |
| 项目 | PROJECT | 本项目数据 | `WHERE project_id = $current_project` |
| 部门 | DEPARTMENT | 本部门数据 | `WHERE project_id = $current_project AND dept_id = $current_dept` |
| 本人 | SELF | 仅本人数据 | `WHERE assigned_to = $current_user` |
### 6.2 数据权限工具类
```java
public final class DataScopeHelper {
public static String getDataFilterSql(String tableAlias, String creatorField,
String deptField, String projectField) {
DataScopeInfo scope = DataScopeContext.getDataScopeInfo();
String prefix = tableAlias != null ? tableAlias + "." : "";
return switch (scope.getScope()) {
case ALL -> "1=1";
case PROJECT -> prefix + projectField + " = '" + scope.getProjectId() + "'";
case DEPARTMENT -> prefix + projectField + " = '" + scope.getProjectId() + "' " +
"AND " + prefix + deptField + " = '" + scope.getDepartmentId() + "'";
case SELF -> prefix + creatorField + " = '" + scope.getUserId() + "'";
};
}
public static boolean canAccessData(UUID dataProjectId, UUID dataDeptId, UUID dataCreatorId) {
DataScopeInfo scope = DataScopeContext.getDataScopeInfo();
return switch (scope.getScope()) {
case ALL -> true;
case PROJECT -> dataProjectId.equals(scope.getProjectId());
case DEPARTMENT -> dataProjectId.equals(scope.getProjectId()) &&
(dataDeptId == null || dataDeptId.equals(scope.getDepartmentId()));
case SELF -> dataCreatorId.equals(scope.getUserId());
};
}
}
```
---
## 七、状态驱动权限
### 7.1 工单状态与操作映射
```java
public enum WorkOrderStatus {
CREATED("已创建"),
ASSIGNED("已分配"),
ACCEPTED("已接单"),
IN_PROGRESS("处理中"),
PENDING("待确认"),
COMPLETED("已完成"),
CLOSED("已关闭"),
SUSPENDED("已挂起"),
RETURNED("已退回");
// 获取该状态下可执行的操作
public Set<WorkOrderAction> getAllowedActions() {
return switch (this) {
case CREATED -> Set.of(WorkOrderAction.ASSIGN, WorkOrderAction.EDIT, WorkOrderAction.DELETE);
case ASSIGNED -> Set.of(WorkOrderAction.ACCEPT, WorkOrderAction.TRANSFER,
WorkOrderAction.SUSPEND, WorkOrderAction.RETURN);
case ACCEPTED -> Set.of(WorkOrderAction.START, WorkOrderAction.TRANSFER, WorkOrderAction.SUSPEND);
case IN_PROGRESS -> Set.of(WorkOrderAction.COMPLETE, WorkOrderAction.REPORT_FEE, WorkOrderAction.SUSPEND);
case PENDING -> Set.of(WorkOrderAction.COMPLETE, WorkOrderAction.AUDIT_QUALITY);
case COMPLETED -> Set.of(WorkOrderAction.EVALUATE, WorkOrderAction.CLOSE, WorkOrderAction.AUDIT_FEE);
case CLOSED -> Set.of();
case SUSPENDED -> Set.of(WorkOrderAction.RESUME);
case RETURNED -> Set.of(WorkOrderAction.ASSIGN, WorkOrderAction.DELETE);
};
}
}
public enum WorkOrderAction {
VIEW, CREATE, EDIT, DELETE,
ASSIGN, ACCEPT, START, COMPLETE, TRANSFER, CLOSE, SUSPEND, RESUME, RETURN,
EVALUATE, REPORT_FEE, AUDIT_FEE, AUDIT_QUALITY
}
```
### 7.2 权限校验增强
```java
// WorkOrderServiceImpl.java
public WorkOrder assign(UUID workOrderId, UUID assigneeId, String assigneeName) {
WorkOrder workOrder = getWorkOrder(workOrderId);
UUID currentUserId = getCurrentUserId();
// 1. 状态校验
validateStatusTransition(workOrder.getStatus(), WorkOrderStatus.ASSIGNED);
// 2. 权限校验
if (!hasActionPermission(workOrder, WorkOrderAction.ASSIGN)) {
throw new PermissionDeniedException("无分配工单权限");
}
// 3. 数据权限校验
if (!canAccessWorkOrder(workOrder)) {
throw new PermissionDeniedException("无权访问该工单");
}
// 执行分配逻辑...
}
private boolean hasActionPermission(WorkOrder workOrder, WorkOrderAction action) {
// 1. 检查状态是否允许该操作
if (!workOrder.getStatus().getAllowedActions().contains(action)) {
return false;
}
// 2. 检查用户是否有该操作的权限编码
String permissionCode = "ops:work_order:" + action.name().toLowerCase();
return permissionService.hasPermission(getCurrentUserId(), getProjectId(), permissionCode);
}
private boolean canAccessWorkOrder(WorkOrder workOrder) {
DataScopeInfo scope = dataScopeContext.getDataScopeInfo();
return switch (scope.getScope()) {
case ALL -> true;
case PROJECT -> workOrder.getProjectId().equals(scope.getProjectId());
case DEPARTMENT -> workOrder.getDepartmentId().equals(scope.getDepartmentId());
case SELF -> workOrder.getAssigneeId().equals(scope.getUserId()) ||
workOrder.getCreatorId().equals(scope.getUserId());
};
}
```
---
## 八、访客凭证管理
### 8.1 访客凭证设计
```java
@Entity
@Table(name = "auth_visitor_credential")
@Data
public class VisitorCredential {
@Id
private UUID id;
private UUID projectId;
private String visitorName;
private String visitorPhone;
private String visitorIdCard;
private String credentialCode;
private String qrCode;
private LocalDateTime expireTime;
private UUID spaceNodeId;
private String accessGates;
private CredentialStatus status;
private LocalDateTime firstUseTime;
private LocalDateTime lastUseTime;
private Integer useCount;
private UUID creatorId;
private String creatorName;
private LocalDateTime createdAt;
}
public enum CredentialStatus {
ACTIVE("有效"),
USED("已使用"),
EXPIRED("已过期"),
REVOKED("已撤销");
}
```
### 8.2 二维码生成与验证
```java
@Service
public class VisitorCredentialService {
@Autowired
private VisitorCredentialRepository credentialRepository;
public VisitorCredential generateCredential(VisitorCredentialRequest request) {
VisitorCredential credential = new VisitorCredential();
credential.setId(UUID.randomUUID());
credential.setProjectId(request.getProjectId());
credential.setVisitorName(request.getVisitorName());
credential.setVisitorPhone(request.getVisitorPhone());
credential.setVisitorIdCard(request.getVisitorIdCard());
credential.setSpaceNodeId(request.getSpaceNodeId());
credential.setAccessGates(JsonUtils.toJson(request.getAccessGates()));
credential.setStatus(CredentialStatus.ACTIVE);
credential.setExpireTime(LocalDateTime.now().plusHours(24));
credential.setUseCount(0);
credential.setCreatorId(request.getCreatorId());
credential.setCreatedAt(LocalDateTime.now());
String qrContent = generateQrContent(credential);
credential.setQrCode(qrContent);
return credentialRepository.save(credential);
}
private String generateQrContent(VisitorCredential credential) {
String data = String.format("VC:%s:%d",
credential.getId(),
System.currentTimeMillis());
String signature = generateSignature(data);
return data + ":" + signature;
}
public CredentialVerifyResult verifyCredential(String qrCode, String gateCode) {
String[] parts = qrCode.split(":");
if (parts.length != 4 || !"VC".equals(parts[0])) {
return CredentialVerifyResult.fail("无效的二维码格式");
}
UUID credentialId = UUID.fromString(parts[1]);
VisitorCredential credential = credentialRepository.findById(credentialId)
.orElse(null);
if (credential == null) {
return CredentialVerifyResult.fail("凭证不存在");
}
if (credential.getStatus() == CredentialStatus.EXPIRED ||
credential.getExpireTime().isBefore(LocalDateTime.now())) {
return CredentialVerifyResult.fail("凭证已过期");
}
if (credential.getStatus() == CredentialStatus.REVOKED) {
return CredentialVerifyResult.fail("凭证已撤销");
}
List<String> accessGates = JsonUtils.fromJson(credential.getAccessGates(), List.class);
if (!accessGates.contains(gateCode)) {
return CredentialVerifyResult.fail("无此门禁通行权限");
}
credential.setUseCount(credential.getUseCount() + 1);
credential.setLastUseTime(LocalDateTime.now());
if (credential.getFirstUseTime() == null) {
credential.setFirstUseTime(LocalDateTime.now());
}
credentialRepository.save(credential);
return CredentialVerifyResult.success(credential);
}
}
```
---
## 九、操作日志审计
### 9.1 操作日志设计
```java
@Entity
@Table(name = "auth_operation_log")
@Data
public class OperationLog {
@Id
private UUID id;
private UUID projectId;
private UUID userId;
private String username;
private String realName;
private String module;
private String operation;
private String description;
private String requestMethod;
private String requestUrl;
private String requestParams;
private String requestBody;
private Integer responseStatus;
private String responseBody;
private Long executionTime;
private String ipAddress;
private String userAgent;
private Boolean hasError;
private String errorMessage;
private LocalDateTime createdAt;
}
```
### 9.2 日志记录切面
```java
@Aspect
@Component
@Slf4j
public class OperationLogAspect {
@Autowired
private OperationLogRepository operationLogRepository;
@Around("@annotation(operationLog)")
public Object around(ProceedingJoinPoint point, LogOperation operationLog) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
UUID userId = SecurityUtils.getCurrentUserId();
String username = SecurityUtils.getCurrentUsername();
OperationLog logEntry = new OperationLog();
logEntry.setId(UUID.randomUUID());
logEntry.setProjectId(ProjectContextHolder.getCurrentProjectId());
logEntry.setUserId(userId);
logEntry.setUsername(username);
logEntry.setModule(operationLog.module());
logEntry.setOperation(operationLog.operation());
logEntry.setDescription(operationLog.description());
logEntry.setRequestMethod(request.getMethod());
logEntry.setRequestUrl(request.getRequestURI());
logEntry.setIpAddress(getClientIp(request));
logEntry.setUserAgent(request.getHeader("User-Agent"));
logEntry.setCreatedAt(LocalDateTime.now());
long startTime = System.currentTimeMillis();
try {
Object result = point.proceed();
logEntry.setResponseStatus(200);
logEntry.setHasError(false);
return result;
} catch (Exception e) {
logEntry.setHasError(true);
logEntry.setErrorMessage(e.getMessage());
throw e;
} finally {
logEntry.setExecutionTime(System.currentTimeMillis() - startTime);
operationLogRepository.save(logEntry);
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
String module();
String operation();
String description();
String businessType() default "";
}
```
---
## 十、API 接口
### 10.1 认证接口
```java
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "认证管理")
public class AuthController {
@PostMapping("/login")
@Operation(summary = "用户登录")
public Result<LoginResponse> login(@RequestBody @Valid LoginRequest request);
@PostMapping("/logout")
@Operation(summary = "用户登出")
public Result<Void> logout();
@PostMapping("/refresh-token")
@Operation(summary = "刷新Token")
public Result<TokenResponse> refreshToken(@RequestBody RefreshTokenRequest request);
@GetMapping("/current-user")
@Operation(summary = "获取当前用户信息")
public Result<UserVO> getCurrentUser();
@PostMapping("/change-password")
@Operation(summary = "修改密码")
public Result<Void> changePassword(@RequestBody @Valid ChangePasswordRequest request);
@GetMapping("/user-permissions")
@Operation(summary = "获取用户权限信息")
public Result<UserPermissionsResponse> getUserPermissions();
}
```
### 10.2 用户管理接口
```java
@RestController
@RequestMapping("/api/v1/auth/users")
@Tag(name = "用户管理")
public class UserController {
@PostMapping
@Operation(summary = "创建用户")
public Result<UserVO> create(@RequestBody @Valid UserCreateRequest request);
@GetMapping("/{id}")
@Operation(summary = "获取用户详情")
public Result<UserVO> getById(@PathVariable UUID id);
@GetMapping
@Operation(summary = "分页查询用户")
public Result<Page<UserVO>> page(UserQueryRequest request);
@PutMapping("/{id}")
@Operation(summary = "更新用户")
public Result<UserVO> update(@PathVariable UUID id, @RequestBody @Valid UserUpdateRequest request);
@DeleteMapping("/{id}")
@Operation(summary = "删除用户")
public Result<Void> delete(@PathVariable UUID id);
@PostMapping("/{id}/roles")
@Operation(summary = "分配角色")
public Result<Void> assignRoles(@PathVariable UUID id, @RequestBody UserRoleAssignRequest request);
@PostMapping("/{id}/status")
@Operation(summary = "修改用户状态")
public Result<Void> changeStatus(@PathVariable UUID id, @RequestBody UserStatusChangeRequest request);
}
```
### 10.3 角色权限接口
```java
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "角色权限管理")
public class RolePermissionController {
@PostMapping("/roles")
@Operation(summary = "创建角色")
public Result<RoleVO> createRole(@RequestBody @Valid RoleCreateRequest request);
@GetMapping("/roles")
@Operation(summary = "查询角色列表")
public Result<List<RoleVO>> listRoles();
@PutMapping("/roles/{id}")
@Operation(summary = "更新角色")
public Result<RoleVO> updateRole(@PathVariable UUID id, @RequestBody @Valid RoleUpdateRequest request);
@DeleteMapping("/roles/{id}")
@Operation(summary = "删除角色")
public Result<Void> deleteRole(@PathVariable UUID id);
@PostMapping("/roles/{id}/permissions")
@Operation(summary = "分配权限")
public Result<Void> assignPermissions(@PathVariable UUID id, @RequestBody RolePermissionAssignRequest request);
@GetMapping("/permissions")
@Operation(summary = "查询权限列表")
public Result<List<PermissionVO>> listPermissions();
@GetMapping("/permissions/tree")
@Operation(summary = "获取权限树")
public Result<List<PermissionTreeVO>> getPermissionTree();
@GetMapping("/users/{id}/permissions")
@Operation(summary = "获取用户权限")
public Result<List<PermissionVO>> getUserPermissions(@PathVariable UUID id);
@GetMapping("/users/{id}/menus")
@Operation(summary = "获取用户菜单")
public Result<List<MenuVO>> getUserMenus(@PathVariable UUID id);
@PostMapping("/check")
@Operation(summary = "校验权限")
public Result<Boolean> checkPermission(@RequestBody PermissionCheckRequest request);
}
```
---
## 十一、实现状态与差异
### 11.1 实现状态
| 功能模块 | 实现状态 | 备注 |
|---------|---------|------|
| User | 🟢 已实现 | 基础CRUD |
| Role | 🟢 已实现 | 基础CRUD |
| Permission | 🟢 已实现 | 基础CRUD |
| JWT认证 | 🟢 已实现 | Token生成/验证 |
| 项目上下文 | 🟢 已实现 | 项目隔离 |
| 访客凭证 | 🟢 已实现 | 二维码生成/验证 |
| 数据权限 | 🟡 部分实现 | 框架已搭建需补充PROJECT级别 |
| 操作日志 | 🟢 已实现 | AOP切面记录 |
| 按钮级权限 | ⏳ 待实现 | 细粒度操作权限 |
| 状态驱动权限 | ⏳ 待实现 | 根据业务状态控制操作 |
| 字段脱敏 | ⏳ 待实现 | 敏感字段脱敏 |
### 11.2 改进计划
| 优先级 | 改进项 | 说明 |
|--------|--------|------|
| P0 | 按钮级权限控制 | 新增操作按钮权限编码 |
| P0 | 状态驱动权限 | 工单状态与操作联动 |
| P1 | 数据权限完善 | 补充PROJECT级别 |
| P1 | 扫码强制机制 | 巡检扫码策略配置 |
| P2 | 字段脱敏 | 敏感字段脱敏 |
| P2 | 角色业务属性 | 新增业务类型、终端类型字段 |
---
## 十二、数据库表结构
```sql
-- 用户表
CREATE TABLE auth_user (
id UUID PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
salt VARCHAR(20) NOT NULL,
real_name VARCHAR(100),
phone VARCHAR(20),
email VARCHAR(100),
avatar VARCHAR(255),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
last_login_time TIMESTAMP,
last_login_ip VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by UUID,
UNIQUE(username)
);
-- 角色表
CREATE TABLE auth_role (
id UUID PRIMARY KEY,
project_id UUID, -- NULL表示系统级角色
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
description VARCHAR(500),
type VARCHAR(20) NOT NULL DEFAULT 'CUSTOM',
data_scope VARCHAR(20) DEFAULT 'SELF',
business_type VARCHAR(30), -- 新增:业务类型
terminal_type VARCHAR(20), -- 新增:终端类型
is_frontline BOOLEAN DEFAULT FALSE, -- 新增:是否一线人员
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(project_id, code)
);
-- 权限表
CREATE TABLE auth_permission (
id UUID PRIMARY KEY,
project_id UUID, -- NULL表示系统级权限
name VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL, -- module:resource:action
type VARCHAR(20) NOT NULL,
module VARCHAR(50), -- 新增:模块标识
resource VARCHAR(50), -- 新增:资源类型
action VARCHAR(30), -- 新增:操作类型
path VARCHAR(255),
component VARCHAR(255),
icon VARCHAR(50),
sort_order INTEGER DEFAULT 0,
parent_id UUID,
level INTEGER DEFAULT 0,
api_method VARCHAR(10),
api_path VARCHAR(255),
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(code, project_id)
);
-- 用户角色关联表
CREATE TABLE auth_user_role (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
role_id UUID NOT NULL,
project_id UUID,
is_default BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(user_id, role_id, project_id)
);
-- 角色权限关联表
CREATE TABLE auth_role_permission (
id UUID PRIMARY KEY,
role_id UUID NOT NULL,
permission_id UUID NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(role_id, permission_id)
);
-- 项目表
CREATE TABLE auth_project (
id UUID PRIMARY KEY,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
description VARCHAR(500),
province VARCHAR(50),
city VARCHAR(50),
district VARCHAR(50),
address VARCHAR(255),
manager_id UUID,
manager_name VARCHAR(100),
manager_phone VARCHAR(20),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
start_date DATE,
end_date DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(code)
);
-- 访客凭证表
CREATE TABLE auth_visitor_credential (
id UUID PRIMARY KEY,
project_id UUID NOT NULL,
visitor_name VARCHAR(100) NOT NULL,
visitor_phone VARCHAR(20),
visitor_id_card VARCHAR(18),
credential_code VARCHAR(50) NOT NULL,
qr_code TEXT NOT NULL,
expire_time TIMESTAMP NOT NULL,
space_node_id UUID,
access_gates JSONB,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
first_use_time TIMESTAMP,
last_use_time TIMESTAMP,
use_count INTEGER DEFAULT 0,
creator_id UUID,
creator_name VARCHAR(100),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(credential_code)
);
-- 操作日志表
CREATE TABLE auth_operation_log (
id UUID PRIMARY KEY,
project_id UUID,
user_id UUID,
username VARCHAR(50),
real_name VARCHAR(100),
module VARCHAR(50) NOT NULL,
operation VARCHAR(50) NOT NULL,
description VARCHAR(255),
request_method VARCHAR(10),
request_url VARCHAR(255),
request_params TEXT,
request_body TEXT,
response_status INTEGER,
response_body TEXT,
execution_time BIGINT,
ip_address VARCHAR(50),
user_agent TEXT,
has_error BOOLEAN DEFAULT FALSE,
error_message TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 创建索引
CREATE INDEX idx_user_username ON auth_user(username);
CREATE INDEX idx_user_status ON auth_user(status);
CREATE INDEX idx_role_project ON auth_role(project_id);
CREATE INDEX idx_permission_parent ON auth_permission(parent_id);
CREATE INDEX idx_permission_module ON auth_permission(module);
CREATE INDEX idx_user_role_user ON auth_user_role(user_id);
CREATE INDEX idx_user_role_role ON auth_user_role(role_id);
CREATE INDEX idx_role_permission_role ON auth_role_permission(role_id);
CREATE INDEX idx_credential_project ON auth_visitor_credential(project_id);
CREATE INDEX idx_credential_status ON auth_visitor_credential(status);
CREATE INDEX idx_operation_log_user ON auth_operation_log(user_id);
CREATE INDEX idx_operation_log_created ON auth_operation_log(created_at);
```
---
## 十三、更新记录
| 日期 | 更新内容 | 更新人 |
|------|----------|--------|
| 2026-02-10 | 创建权限与账户领域技术方案 | - |
| 2026-02-27 | 重构角色体系新增13个角色 | - |
| 2026-02-27 | 新增权限编码规范 | - |
| 2026-02-27 | 新增数据权限四级范围 | - |
| 2026-02-27 | 新增状态驱动权限设计 | - |
| 2026-02-27 | 新增角色业务属性字段 | - |
---
**文档维护**: 本领域技术方案由 ether-auth 服务负责人维护