1416 lines
48 KiB
Markdown
1416 lines
48 KiB
Markdown
# 权限与账户领域技术方案
|
||
|
||
**领域编号**: 4.5
|
||
**微服务**: ether-auth
|
||
**最后更新**: 2026-04-26
|
||
|
||
> **更新记录**:
|
||
> - 2026-04-26: 反向同步实际代码实现到文档。主要变更:Role.type 改为 SYSTEM/PROJECT/DEPARTMENT 三级分类;Role.status 改为 RoleStatus 枚举;Permission 结构简化(parentCode 替代 parentId);UserRole 简化为 M2M 中间表;OperationLog 更名为 AuditLog(表名和字段结构变更);新增实体(EnterpriseUser/ProjectStaff/ProjectStaffRole/Resident/ResidentSpace/Space/UserProject/DataAccess/Dept/SysConfig);新增 API(部门管理/项目成员管理/数据访问授权/审计日志/系统配置/企业员工/用户项目关联);新增业务规则(登录失败锁定/密码强度校验/审计日志增强/项目成员角色/部门类型/住户认证);角色体系改为数据库动态管理;API路径去掉v1版本号。标注 TODO 项:Permission 需增加菜单路由属性(path/component/icon)。
|
||
|
||
---
|
||
|
||
## 一、领域概述
|
||
|
||
### 1.1 领域职责
|
||
|
||
权限与账户领域负责 Ether 平台的身份认证与授权管理:
|
||
- 用户认证与登录
|
||
- RBAC 权限模型(角色-权限-用户关联)
|
||
- 项目隔离与数据权限
|
||
- 按钮级权限控制
|
||
- 状态驱动权限
|
||
- 访客凭证管理
|
||
- 操作日志审计
|
||
|
||
### 1.2 核心概念
|
||
|
||
| 概念 | 说明 | 对应实体 |
|
||
|------|------|----------|
|
||
| **用户** | 系统登录账户,支持四种用户类型 | User |
|
||
| **企业员工** | 企业类型用户的扩展信息 | EnterpriseUser |
|
||
| **项目员工** | 项目类型员工的扩展信息,含班次、岗位状态 | ProjectStaff |
|
||
| **住户** | 业主/家属/租户,含认证流程 | Resident |
|
||
| **角色** | 权限集合,支持系统级/项目级/部门级 | Role |
|
||
| **权限** | 功能访问控制点,支持菜单/按钮/API三种类型 | Permission |
|
||
| **部门** | 组织架构节点,支持树形结构 | Dept |
|
||
| **用户-项目关联** | 用户参与项目的多对多关系 | UserProject |
|
||
| **项目员工角色** | 项目员工在项目中的角色分配 | ProjectStaffRole |
|
||
| **数据访问授权** | 细粒度数据访问控制记录(接口预留) | DataAccess |
|
||
| **审计日志** | 操作审计记录 | AuditLog |
|
||
| **房屋空间** | 项目下的房屋/空间信息 | Space |
|
||
| **住户-房屋关联** | 住户与房屋的绑定关系 | ResidentSpace |
|
||
| **系统配置** | 键值对形式的系统参数 | SysConfig |
|
||
|
||
---
|
||
|
||
## 二、角色体系设计
|
||
|
||
### 2.1 角色总览
|
||
|
||
| 类型 | 角色编码 | 角色名称 | 数据范围 | 终端类型 | 一线人员 |
|
||
|------|---------|---------|---------|---------|---------|
|
||
| **系统级** |
|
||
| 系统管理 | SUPER_ADMIN | 超级管理员 | ALL | ALL | 否 |
|
||
| 系统管理 | SYS_ADMIN | 系统管理员 | ALL | ADMIN_ONLY | 否 |
|
||
| **项目管理** |
|
||
| 项目管理 | PROPERTY_MANAGER | 物业经理 | PROJECT | ALL | 否 |
|
||
| 项目管理 | PROJECT_MANAGER | 项目经理 | PROJECT | ALL | 否 |
|
||
| **部门主管** |
|
||
| 工程管理 | ENGINEERING_LEAD | 工程主管 | DEPARTMENT | ALL | 否 |
|
||
| 安保管理 | SECURITY_LEAD | 安保主管 | DEPARTMENT | ALL | 否 |
|
||
| 保洁管理 | CLEANING_LEAD | 保洁主管 | DEPARTMENT | ALL | 否 |
|
||
| 财务管理 | FINANCE_LEAD | 财务主管 | DEPARTMENT | ALL | 否 |
|
||
| **一线执行** |
|
||
| 工程执行 | MAINTENANCE_STAFF | 维修人员 | SELF | MOBILE_ONLY | 是 |
|
||
| 安保执行 | SECURITY_STAFF | 安保人员 | SELF | MOBILE_ONLY | 是 |
|
||
| 保洁执行 | CLEANING_STAFF | 保洁人员 | SELF | MOBILE_ONLY | 是 |
|
||
| **服务支持** |
|
||
| 业主服务 | CS_STAFF | 客服人员 | PROJECT | ALL | 否 |
|
||
| **外部用户** |
|
||
| 业主 | OWNER | 业主 | SELF | OWNER_APP | 否 |
|
||
|
||
### 2.2 角色职责说明
|
||
|
||
#### 2.2.1 系统级角色
|
||
|
||
| 角色 | 职责范围 | 典型场景 |
|
||
|------|---------|---------|
|
||
| **超级管理员** | 系统最高权限,管理所有项目和用户 | 系统初始化、用户创建、项目创建 |
|
||
| **系统管理员** | 系统配置管理,不参与业务操作 | 系统参数配置、日志查看 |
|
||
|
||
#### 2.2.2 项目管理角色
|
||
|
||
| 角色 | 职责范围 | 典型场景 |
|
||
|------|---------|---------|
|
||
| **物业经理** | 项目经营第一负责人,拥有本项目全部权限 | 项目整体运营、费用审核、合同审批 |
|
||
| **项目经理** | 单项目管理,拥有本项目全部业务权限 | 工单管理、人员分配、报表查看 |
|
||
|
||
#### 2.2.3 部门主管角色
|
||
|
||
| 角色 | 管理模块 | 职责范围 | 典型场景 |
|
||
|------|---------|---------|---------|
|
||
| **工程主管** | 工单、设备、工程巡检 | 本部门工程事务管理 | 工单分配、设备管理、巡检计划制定 |
|
||
| **安保主管** | 安保巡检、访客管理 | 本部门安保事务管理 | 安保巡检计划、访客核验监督 |
|
||
| **保洁主管** | 保洁巡检、品质检查 | 本部门保洁事务管理 | 保洁巡检计划、品质检查 |
|
||
| **财务主管** | 账单、收费、费用审核 | 本部门财务事务管理 | 账单生成、费用审核、报表导出 |
|
||
|
||
#### 2.2.4 一线执行角色
|
||
|
||
| 角色 | 执行范围 | 终端限制 | 典型场景 |
|
||
|------|---------|---------|---------|
|
||
| **维修人员** | 工单处理、设备巡检 | 仅移动端 | 接单、维修、填报费用、设备巡检 |
|
||
| **安保人员** | 安保巡检、访客核验 | 仅移动端 | 扫码签到、访客核验、异常报送 |
|
||
| **保洁人员** | 保洁巡检、品质检查 | 仅移动端 | 扫码签到、品质异常报送 |
|
||
|
||
#### 2.2.5 服务支持角色
|
||
|
||
| 角色 | 职责范围 | 典型场景 |
|
||
|------|---------|---------|
|
||
| **客服人员** | 业主服务接口,不参与工单流程 | 业主咨询、资料维护、访客辅助核验 |
|
||
|
||
#### 2.2.6 外部用户角色
|
||
|
||
| 角色 | 权限范围 | 典型场景 |
|
||
|------|---------|---------|
|
||
| **业主** | 本人房产相关数据 | 在线报修、缴费、访客邀请、评价 |
|
||
|
||
---
|
||
|
||
## 三、领域模型
|
||
|
||
### 3.1 聚合根设计
|
||
|
||
#### User(系统用户)
|
||
|
||
```java
|
||
@Entity
|
||
@Table(name = "auth_user")
|
||
@Data
|
||
public class User {
|
||
@Id
|
||
private UUID id;
|
||
|
||
private String username; // 登录账号
|
||
private String password; // BCrypt加密密码
|
||
private String salt; // 密码盐值(BCrypt模式下冗余)
|
||
|
||
private String realName; // 真实姓名
|
||
private String phone;
|
||
private String email;
|
||
private String avatar; // 头像URL
|
||
|
||
private UserStatus status; // ACTIVE/LOCKED/DISABLED
|
||
private UserType userType; // ENTERPRISE/PROJECT_STAFF/RESIDENT/CUSTOMER
|
||
private UUID deptId; // 所属部门ID
|
||
|
||
private LocalDateTime lastLoginTime;
|
||
private String lastLoginIp;
|
||
|
||
@ManyToMany(fetch = FetchType.LAZY)
|
||
@JoinTable(
|
||
name = "auth_user_role",
|
||
joinColumns = @JoinColumn(name = "user_id"),
|
||
inverseJoinColumns = @JoinColumn(name = "role_id")
|
||
)
|
||
private List<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/API(String类型)
|
||
|
||
// 资源信息
|
||
private String resource; // 资源路径(API类型)
|
||
private String method; // HTTP方法(API类型)
|
||
|
||
private String description; // 权限描述
|
||
|
||
// 树形结构(简化:parentCode替代parentId)
|
||
private String parentCode; // 父权限代码(替代原 parentId UUID引用)
|
||
private Integer sortOrder; // 排序序号
|
||
|
||
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "permissions")
|
||
private List<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)、租户ID(tenantId)等字段,并采用异步持久化。
|
||
|
||
```java
|
||
@Entity
|
||
@Table(name = "sys_audit_log")
|
||
@Data
|
||
public class AuditLog {
|
||
@Id
|
||
private UUID id;
|
||
|
||
private UUID userId;
|
||
private String username; // 操作用户名
|
||
private String operation; // 操作描述
|
||
private String module; // 模块标识
|
||
private ActionType action; // 操作类型枚举
|
||
|
||
// 目标追踪(新增)
|
||
private String targetType; // 目标类型
|
||
private String targetId; // 目标ID
|
||
|
||
// 操作详情(替代原 requestBody/responseBody)
|
||
private String content; // 操作内容
|
||
private String params; // 请求参数
|
||
private String result; // 操作结果
|
||
|
||
private String ipAddress;
|
||
private String userAgent;
|
||
private String requestUrl;
|
||
private String requestMethod;
|
||
private Integer executionTimeMs; // 执行耗时(ms)
|
||
|
||
private AuditStatus status; // SUCCESS/FAIL
|
||
private String errorMsg; // 错误信息
|
||
|
||
private UUID tenantId; // 租户ID(新增)
|
||
private LocalDateTime createdAt;
|
||
}
|
||
|
||
public enum ActionType {
|
||
CREATE, UPDATE, DELETE, QUERY, VIEW,
|
||
LOGIN, LOGOUT, EXPORT, IMPORT, ASSIGN, REVOKE;
|
||
}
|
||
|
||
public enum AuditStatus {
|
||
SUCCESS("成功"),
|
||
FAIL("失败");
|
||
}
|
||
```
|
||
|
||
### 9.2 日志记录注解
|
||
|
||
```java
|
||
@Target(ElementType.METHOD)
|
||
@Retention(RetentionPolicy.RUNTIME)
|
||
public @interface OperationLog {
|
||
String operation();
|
||
String module();
|
||
AuditLog.ActionType action();
|
||
}
|
||
```
|
||
|
||
> **业务规则**:
|
||
> - 审计日志通过 `@OperationLog` 注解 + AOP 切面自动记录
|
||
> - 采用异步持久化(`auditLogExecutor` 线程池),避免影响业务性能
|
||
> - 查询窗口强制限制30天,超过30天的查询自动截断起始时间
|
||
> - 归档策略:90天以上日志归档(当前实现为直接删除,TODO: 导出至对象存储)
|
||
> - 记录模块:USER / ROLE / PERMISSION / PROJECT / AUTH / DEPT / PROJECT_MEMBER / SYSTEM
|
||
|
||
---
|
||
|
||
## 十、API 接口
|
||
|
||
> **注意**: 实际代码中 API 路径已去掉 v1 版本号,统一为 `/api/auth` 前缀。
|
||
|
||
### 10.1 认证接口
|
||
|
||
```java
|
||
@RestController
|
||
@RequestMapping("/api/auth")
|
||
@Tag(name = "认证管理")
|
||
public class AuthController {
|
||
|
||
@PostMapping("/login")
|
||
@Operation(summary = "用户登录")
|
||
public Result<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. 检查登录失败锁定(Redis,5次失败锁定10分钟)
|
||
> 2. 查询用户(含角色)
|
||
> 3. 验证密码(BCrypt)
|
||
> 4. 检查用户状态(LOCKED/DISABLED拒绝)
|
||
> 5. 收集所有角色(用户直接角色 + 项目员工角色)
|
||
> 6. 生成JWT Token(含userId、username、roles claims)
|
||
> 7. 返回Token和用户基本信息
|
||
|
||
### 10.2 用户管理接口
|
||
|
||
```java
|
||
@RestController
|
||
@RequestMapping("/api/auth/users")
|
||
@Tag(name = "用户管理")
|
||
public class UserController {
|
||
|
||
@PostMapping
|
||
@Operation(summary = "创建用户")
|
||
public Result<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 服务负责人维护
|