权限与账户领域技术方案
领域编号: 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(系统用户)
@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(角色)
@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(权限)
@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(用户角色关联)
@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(角色权限关联)
@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 认证
@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 项目上下文
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 权限检查
@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 数据权限工具类
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 工单状态与操作映射
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 权限校验增强
// 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 访客凭证设计
@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 二维码生成与验证
@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 操作日志设计
@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 日志记录切面
@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 认证接口
@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 用户管理接口
@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 角色权限接口
@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 |
角色业务属性 |
新增业务类型、终端类型字段 |
十二、数据库表结构
-- 用户表
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 服务负责人维护