feat: add equipment extension fields to SpaceNode entity

This commit is contained in:
chiguyong 2026-03-23 23:53:55 +08:00
parent 7647e858e9
commit aa3f6c4ced
58 changed files with 3897 additions and 204 deletions

BIN
HashPassword.class Normal file

Binary file not shown.

9
HashPassword.java Normal file
View File

@ -0,0 +1,9 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class HashPassword {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String hash = encoder.encode("Admin@123");
System.out.println(hash);
}
}

View File

@ -0,0 +1,60 @@
package com.ether.pms.auth.annotation;
import com.ether.pms.auth.entity.AuditLog;
import java.lang.annotation.*;
/**
* 审计日志注解
* 用于标记需要记录审计日志的方法
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
/**
* 操作描述
*/
String operation();
/**
* 功能模块
*/
String module();
/**
* 操作类型
*/
AuditLog.ActionType action() default AuditLog.ActionType.UPDATE;
/**
* 目标对象类型支持SpEL表达式
*/
String targetType() default "";
/**
* 目标对象ID支持SpEL表达式
*/
String targetId() default "";
/**
* 操作内容支持SpEL表达式
*/
String content() default "";
/**
* 是否记录请求参数
*/
boolean recordParams() default true;
/**
* 是否记录响应结果
*/
boolean recordResult() default false;
/**
* 需要过滤的敏感字段不记录
*/
String[] sensitiveFields() default {"password", "token", "secret", "creditCard"};
}

View File

@ -0,0 +1,179 @@
package com.ether.pms.auth.aspect;
import com.ether.pms.auth.annotation.OperationLog;
import com.ether.pms.auth.entity.AuditLog.ActionType;
import com.ether.pms.auth.entity.AuditLog.Status;
import com.ether.pms.auth.service.AuditLogService;
import com.ether.pms.auth.util.SecurityUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class AuditLogAspect {
private final AuditLogService auditLogService;
private final ObjectMapper objectMapper;
@Around("@annotation(com.ether.pms.auth.annotation.OperationLog)")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
OperationLog auditLogAnnotation = method.getAnnotation(OperationLog.class);
// 获取请求信息
HttpServletRequest request = getRequest();
if (request == null) {
return point.proceed();
}
// 记录开始时间
long startTime = System.currentTimeMillis();
// 创建审计日志对象
com.ether.pms.auth.entity.AuditLog auditLog = new com.ether.pms.auth.entity.AuditLog();
auditLog.setOperation(auditLogAnnotation.operation());
auditLog.setModule(auditLogAnnotation.module());
auditLog.setAction(auditLogAnnotation.action());
auditLog.setRequestUrl(request.getRequestURI());
auditLog.setRequestMethod(request.getMethod());
auditLog.setIpAddress(getClientIp(request));
auditLog.setUserAgent(request.getHeader("User-Agent"));
// 获取当前用户信息
UUID userId = SecurityUtils.getCurrentUserId();
String username = SecurityUtils.getCurrentUsername();
if (userId != null) {
auditLog.setUserId(userId);
}
auditLog.setUsername(username != null ? username : "anonymous");
// 记录请求参数
if (auditLogAnnotation.recordParams()) {
try {
Object[] args = point.getArgs();
String params = filterSensitiveData(args, auditLogAnnotation.sensitiveFields());
auditLog.setParams(params);
} catch (Exception e) {
log.warn("记录请求参数失败: {}", e.getMessage());
}
}
Object result = null;
try {
// 执行目标方法
result = point.proceed();
// 设置成功状态
auditLog.setStatus(Status.SUCCESS);
// 记录响应结果
if (auditLogAnnotation.recordResult() && result != null) {
try {
auditLog.setResult(objectMapper.writeValueAsString(result));
} catch (Exception e) {
log.warn("记录响应结果失败: {}", e.getMessage());
}
}
return result;
} catch (Throwable throwable) {
// 设置失败状态
auditLog.setStatus(Status.FAIL);
auditLog.setErrorMsg(throwable.getMessage());
throw throwable;
} finally {
// 计算执行时间
long executionTime = System.currentTimeMillis() - startTime;
auditLog.setExecutionTimeMs((int) executionTime);
// 异步保存审计日志
auditLogService.saveAuditLogAsync(auditLog);
}
}
private HttpServletRequest getRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
return attributes.getRequest();
}
return null;
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理情况取第一个IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
private String filterSensitiveData(Object[] args, String[] sensitiveFields) {
try {
if (args == null || args.length == 0) {
return "[]";
}
Set<String> sensitiveFieldSet = new HashSet<>(Arrays.asList(sensitiveFields));
ObjectNode rootNode = objectMapper.createObjectNode();
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg == null) {
rootNode.putNull("arg" + i);
continue;
}
// 跳过HttpServletRequest等不需要记录的参数
if (arg instanceof HttpServletRequest) {
continue;
}
String argJson = objectMapper.writeValueAsString(arg);
ObjectNode argNode = (ObjectNode) objectMapper.readTree(argJson);
// 过滤敏感字段
for (String field : sensitiveFieldSet) {
if (argNode.has(field)) {
argNode.put(field, "***");
}
}
rootNode.set("arg" + i, argNode);
}
return objectMapper.writeValueAsString(rootNode);
} catch (Exception e) {
log.warn("过滤敏感数据失败: {}", e.getMessage());
return "[]";
}
}
}

View File

@ -0,0 +1,28 @@
package com.ether.pms.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
/**
* 审计日志异步执行器
*/
@Bean("auditLogExecutor")
public Executor auditLogExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("audit-log-");
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

View File

@ -0,0 +1,94 @@
package com.ether.pms.auth.controller;
import com.ether.pms.auth.entity.AuditLog;
import com.ether.pms.auth.service.AuditLogService;
import com.ether.pms.common.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/audit-logs")
@RequiredArgsConstructor
@Slf4j
public class AuditLogController {
private final AuditLogService auditLogService;
/**
* 查询审计日志列表最近30天
*/
@GetMapping
public ApiResponse<Page<AuditLog>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String module,
@RequestParam(required = false) String action,
@RequestParam(required = false) String username,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate) {
// 限制单次查询最大条数
if (size > 100) {
size = 100;
}
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
Page<AuditLog> result = auditLogService.searchLogs(module, action, username, startDate, endDate, pageable);
return ApiResponse.success(result);
}
/**
* 获取模块列表用于筛选
*/
@GetMapping("/modules")
public ApiResponse<List<Map<String, String>>> getModules() {
List<Map<String, String>> modules = Arrays.asList(
Map.of("value", "USER", "label", "用户管理"),
Map.of("value", "ROLE", "label", "角色管理"),
Map.of("value", "PERMISSION", "label", "权限管理"),
Map.of("value", "PROJECT", "label", "项目管理"),
Map.of("value", "AUTH", "label", "登录认证")
);
return ApiResponse.success(modules);
}
/**
* 获取操作类型列表用于筛选
*/
@GetMapping("/actions")
public ApiResponse<List<Map<String, String>>> getActions() {
List<Map<String, String>> actions = Arrays.stream(AuditLog.ActionType.values())
.map(action -> Map.of(
"value", action.name(),
"label", action.getDesc()
))
.collect(Collectors.toList());
return ApiResponse.success(actions);
}
/**
* 获取最近30天的日志统计
*/
@GetMapping("/stats")
public ApiResponse<Map<String, Object>> getStats() {
long count = auditLogService.getRecentLogCount();
return ApiResponse.success(Map.of(
"total", count,
"retentionDays", 30,
"description", "最近30天的审计日志数量"
));
}
}

View File

@ -1,6 +1,10 @@
package com.ether.pms.auth.controller; package com.ether.pms.auth.controller;
import com.ether.pms.auth.annotation.OperationLog;
import com.ether.pms.auth.entity.AuditLog;
import com.ether.pms.auth.entity.Permission;
import com.ether.pms.auth.entity.Role; import com.ether.pms.auth.entity.Role;
import com.ether.pms.auth.entity.User;
import com.ether.pms.auth.service.RoleService; import com.ether.pms.auth.service.RoleService;
import com.ether.pms.common.ApiResponse; import com.ether.pms.common.ApiResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -33,26 +37,40 @@ public class RoleController {
} }
@PostMapping @PostMapping
@OperationLog(operation = "创建角色", module = "ROLE", action = AuditLog.ActionType.CREATE)
public ResponseEntity<ApiResponse<Role>> create(@RequestBody Role role) { public ResponseEntity<ApiResponse<Role>> create(@RequestBody Role role) {
return ResponseEntity.ok(ApiResponse.success(roleService.create(role))); return ResponseEntity.ok(ApiResponse.success(roleService.create(role)));
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@OperationLog(operation = "更新角色", module = "ROLE", action = AuditLog.ActionType.UPDATE)
public ResponseEntity<ApiResponse<Role>> update(@PathVariable UUID id, @RequestBody Role role) { public ResponseEntity<ApiResponse<Role>> update(@PathVariable UUID id, @RequestBody Role role) {
return ResponseEntity.ok(ApiResponse.success(roleService.update(id, role))); return ResponseEntity.ok(ApiResponse.success(roleService.update(id, role)));
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@OperationLog(operation = "删除角色", module = "ROLE", action = AuditLog.ActionType.DELETE)
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) { public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) {
roleService.delete(id); roleService.delete(id);
return ResponseEntity.ok(ApiResponse.success()); return ResponseEntity.ok(ApiResponse.success());
} }
@PostMapping("/{id}/permissions") @PostMapping("/{id}/permissions")
@OperationLog(operation = "分配权限", module = "ROLE", action = AuditLog.ActionType.ASSIGN)
public ResponseEntity<ApiResponse<Void>> assignPermissions( public ResponseEntity<ApiResponse<Void>> assignPermissions(
@PathVariable UUID id, @PathVariable UUID id,
@RequestBody List<UUID> permissionIds) { @RequestBody List<UUID> permissionIds) {
roleService.assignPermissions(id, permissionIds); roleService.assignPermissions(id, permissionIds);
return ResponseEntity.ok(ApiResponse.success()); return ResponseEntity.ok(ApiResponse.success());
} }
@GetMapping("/{id}/permissions")
public ResponseEntity<ApiResponse<List<Permission>>> getPermissions(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(roleService.getPermissions(id)));
}
@GetMapping("/{id}/users")
public ResponseEntity<ApiResponse<List<User>>> getUsersByRoleId(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(roleService.getUsersByRoleId(id)));
}
} }

View File

@ -1,6 +1,8 @@
package com.ether.pms.auth.controller; package com.ether.pms.auth.controller;
import com.ether.pms.auth.annotation.OperationLog;
import com.ether.pms.auth.controller.dto.UserProjectRequest; import com.ether.pms.auth.controller.dto.UserProjectRequest;
import com.ether.pms.auth.entity.AuditLog;
import com.ether.pms.auth.entity.User; import com.ether.pms.auth.entity.User;
import com.ether.pms.auth.entity.UserProject; import com.ether.pms.auth.entity.UserProject;
import com.ether.pms.auth.service.UserProjectService; import com.ether.pms.auth.service.UserProjectService;
@ -33,30 +35,35 @@ public class UserController {
} }
@PostMapping @PostMapping
@OperationLog(operation = "创建用户", module = "USER", action = AuditLog.ActionType.CREATE)
public ResponseEntity<ApiResponse<User>> create(@RequestBody User user) { public ResponseEntity<ApiResponse<User>> create(@RequestBody User user) {
return ResponseEntity.ok(ApiResponse.success(userService.create(user))); return ResponseEntity.ok(ApiResponse.success(userService.create(user)));
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@OperationLog(operation = "更新用户", module = "USER", action = AuditLog.ActionType.UPDATE)
public ResponseEntity<ApiResponse<User>> update(@PathVariable UUID id, @RequestBody User user) { public ResponseEntity<ApiResponse<User>> update(@PathVariable UUID id, @RequestBody User user) {
return ResponseEntity.ok(ApiResponse.success(userService.update(id, user))); return ResponseEntity.ok(ApiResponse.success(userService.update(id, user)));
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@OperationLog(operation = "删除用户", module = "USER", action = AuditLog.ActionType.DELETE)
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) { public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) {
userService.delete(id); userService.delete(id);
return ResponseEntity.ok(ApiResponse.success()); return ResponseEntity.ok(ApiResponse.success());
} }
@PutMapping("/{id}/password") @PutMapping("/{id}/password")
@OperationLog(operation = "修改密码", module = "USER", action = AuditLog.ActionType.UPDATE)
public ResponseEntity<ApiResponse<Void>> updatePassword( public ResponseEntity<ApiResponse<Void>> updatePassword(
@PathVariable UUID id, @PathVariable UUID id,
@RequestBody PasswordRequest request) { @RequestBody PasswordRequest request) {
userService.updatePassword(id, request.getOldPassword(), request.getNewPassword()); userService.updatePassword(id, request.getOldPassword(), request.getNewPassword());
return ResponseEntity.ok(ApiResponse.success()); return ResponseEntity.ok(ApiResponse.success());
} }
@PostMapping("/{id}/roles") @PostMapping("/{id}/roles")
@OperationLog(operation = "分配角色", module = "USER", action = AuditLog.ActionType.ASSIGN)
public ResponseEntity<ApiResponse<Void>> assignRoles( public ResponseEntity<ApiResponse<Void>> assignRoles(
@PathVariable UUID id, @PathVariable UUID id,
@RequestBody List<UUID> roleIds) { @RequestBody List<UUID> roleIds) {

View File

@ -0,0 +1,53 @@
package com.ether.pms.auth.controller.dto;
import com.ether.pms.auth.entity.Role;
import com.ether.pms.auth.entity.User;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Data
public class RoleWithUsersDTO {
private UUID id;
private String code;
private String name;
private String description;
private Role.RoleType type;
private Role.DataScope dataScope;
private Role.RoleStatus status;
private LocalDateTime createdAt;
private List<UserInfo> users;
@Data
public static class UserInfo {
private UUID id;
private String username;
private String realName;
private User.UserStatus status;
public static UserInfo fromUser(User user) {
UserInfo info = new UserInfo();
info.setId(user.getId());
info.setUsername(user.getUsername());
info.setRealName(user.getRealName());
info.setStatus(user.getStatus());
return info;
}
}
public static RoleWithUsersDTO fromRole(Role role) {
RoleWithUsersDTO dto = new RoleWithUsersDTO();
dto.setId(role.getId());
dto.setCode(role.getCode());
dto.setName(role.getName());
dto.setDescription(role.getDescription());
dto.setType(role.getType());
dto.setDataScope(role.getDataScope());
dto.setStatus(role.getStatus());
dto.setCreatedAt(role.getCreatedAt());
return dto;
}
}

View File

@ -0,0 +1,60 @@
package com.ether.pms.auth.controller.dto;
import com.ether.pms.auth.entity.Role;
import com.ether.pms.auth.entity.User;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Data
public class UserWithRolesDTO {
private UUID id;
private String username;
private String realName;
private String phone;
private String email;
private String avatar;
private User.UserStatus status;
private LocalDateTime lastLoginTime;
private String lastLoginIp;
private LocalDateTime createdAt;
private List<RoleInfo> roles;
@Data
public static class RoleInfo {
private UUID id;
private String code;
private String name;
public static RoleInfo fromRole(Role role) {
RoleInfo info = new RoleInfo();
info.setId(role.getId());
info.setCode(role.getCode());
info.setName(role.getName());
return info;
}
}
public static UserWithRolesDTO fromUser(User user) {
UserWithRolesDTO dto = new UserWithRolesDTO();
dto.setId(user.getId());
dto.setUsername(user.getUsername());
dto.setRealName(user.getRealName());
dto.setPhone(user.getPhone());
dto.setEmail(user.getEmail());
dto.setAvatar(user.getAvatar());
dto.setStatus(user.getStatus());
dto.setLastLoginTime(user.getLastLoginTime());
dto.setLastLoginIp(user.getLastLoginIp());
dto.setCreatedAt(user.getCreatedAt());
if (user.getRoles() != null) {
dto.setRoles(user.getRoles().stream()
.map(RoleInfo::fromRole)
.collect(Collectors.toList()));
}
return dto;
}
}

View File

@ -0,0 +1,125 @@
package com.ether.pms.auth.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "sys_audit_log", indexes = {
@Index(name = "idx_audit_log_created_at", columnList = "createdAt"),
@Index(name = "idx_audit_log_user_id", columnList = "userId"),
@Index(name = "idx_audit_log_module", columnList = "module"),
@Index(name = "idx_audit_log_action", columnList = "action")
})
@Data
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "user_id")
private UUID userId;
@Column(nullable = false, length = 64)
private String username;
@Column(nullable = false, length = 128)
private String operation;
@Column(nullable = false, length = 64)
private String module;
@Column(nullable = false, length = 64)
@Enumerated(EnumType.STRING)
private ActionType action;
@Column(length = 64)
private String targetType;
@Column(length = 64)
private String targetId;
@Column(columnDefinition = "TEXT")
private String content;
@Lob
@JdbcTypeCode(SqlTypes.LONGVARCHAR)
private String params;
@Lob
@JdbcTypeCode(SqlTypes.LONGVARCHAR)
private String result;
@Column(name = "ip_address", length = 64)
private String ipAddress;
@Column(length = 512)
private String userAgent;
@Column(name = "request_url", length = 512)
private String requestUrl;
@Column(name = "request_method", length = 16)
private String requestMethod;
@Column(name = "execution_time_ms")
private Integer executionTimeMs;
@Column(length = 16)
@Enumerated(EnumType.STRING)
private Status status = Status.SUCCESS;
@Column(name = "error_msg", columnDefinition = "TEXT")
private String errorMsg;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "tenant_id")
private UUID tenantId;
public enum ActionType {
CREATE("创建"),
UPDATE("修改"),
DELETE("删除"),
QUERY("查询"),
LOGIN("登录"),
LOGOUT("登出"),
EXPORT("导出"),
IMPORT("导入"),
ASSIGN("分配"),
REVOKE("撤销");
private final String desc;
ActionType(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
public enum Status {
SUCCESS("成功"),
FAIL("失败");
private final String desc;
Status(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
}

View File

@ -26,8 +26,7 @@ public class User {
private String username; private String username;
@NotNull(message = "密码不能为空") @NotNull(message = "密码不能为空")
@Size(min = 8, max = 20, message = "密码长度必须在8-20位之间") @Column(nullable = false, length = 255)
@Column(nullable = false)
private String password; private String password;
private String salt; private String salt;

View File

@ -0,0 +1,50 @@
package com.ether.pms.auth.repository;
import com.ether.pms.auth.entity.AuditLog;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@Repository
public interface AuditLogRepository extends JpaRepository<AuditLog, UUID>, JpaSpecificationExecutor<AuditLog> {
/**
* 查询最近30天的审计日志分页
*/
@Query("SELECT a FROM AuditLog a WHERE a.createdAt >= :startTime ORDER BY a.createdAt DESC")
Page<AuditLog> findRecentLogs(@Param("startTime") LocalDateTime startTime, Pageable pageable);
/**
* 查询最近30天的审计日志不分页
*/
@Query("SELECT a FROM AuditLog a WHERE a.createdAt >= :startTime ORDER BY a.createdAt DESC")
List<AuditLog> findRecentLogs(@Param("startTime") LocalDateTime startTime);
/**
* 删除超过指定时间的日志归档用
*/
@Modifying
@Query("DELETE FROM AuditLog a WHERE a.createdAt < :cutoffTime")
int deleteByCreatedAtBefore(@Param("cutoffTime") LocalDateTime cutoffTime);
/**
* 查询超过指定时间的日志归档用
*/
@Query("SELECT a FROM AuditLog a WHERE a.createdAt < :cutoffTime ORDER BY a.createdAt DESC")
List<AuditLog> findByCreatedAtBefore(@Param("cutoffTime") LocalDateTime cutoffTime);
/**
* 统计最近30天的日志数量
*/
@Query("SELECT COUNT(a) FROM AuditLog a WHERE a.createdAt >= :startTime")
long countRecentLogs(@Param("startTime") LocalDateTime startTime);
}

View File

@ -1,6 +1,7 @@
package com.ether.pms.auth.repository; package com.ether.pms.auth.repository;
import com.ether.pms.auth.entity.Role; import com.ether.pms.auth.entity.Role;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
@ -9,12 +10,15 @@ import java.util.UUID;
@Repository @Repository
public interface RoleRepository extends JpaRepository<Role, UUID> { public interface RoleRepository extends JpaRepository<Role, UUID> {
Optional<Role> findByCode(String code); Optional<Role> findByCode(String code);
boolean existsByCode(String code); boolean existsByCode(String code);
List<Role> findByProjectId(String projectId); List<Role> findByProjectId(String projectId);
List<Role> findByType(Role.RoleType type); List<Role> findByType(Role.RoleType type);
@EntityGraph(attributePaths = {"permissions"})
Optional<Role> findWithPermissionsById(UUID id);
} }

View File

@ -1,6 +1,8 @@
package com.ether.pms.auth.repository; package com.ether.pms.auth.repository;
import com.ether.pms.auth.entity.UserProject; import com.ether.pms.auth.entity.UserProject;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
@ -15,6 +17,8 @@ public interface UserProjectRepository extends JpaRepository<UserProject, UUID>
List<UserProject> findByProjectId(UUID projectId); List<UserProject> findByProjectId(UUID projectId);
Page<UserProject> findByProjectId(UUID projectId, Pageable pageable);
@Query("SELECT up.projectId FROM UserProject up WHERE up.userId = :userId") @Query("SELECT up.projectId FROM UserProject up WHERE up.userId = :userId")
List<UUID> findProjectIdsByUserId(@Param("userId") UUID userId); List<UUID> findProjectIdsByUserId(@Param("userId") UUID userId);

View File

@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -16,6 +17,15 @@ public interface UserRepository extends JpaRepository<User, UUID> {
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username") @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username")
Optional<User> findByUsernameWithRoles(@Param("username") String username); Optional<User> findByUsernameWithRoles(@Param("username") String username);
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles")
List<User> findAllWithRoles();
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.id = :id")
Optional<User> findByIdWithRoles(@Param("id") UUID id);
@Query("SELECT u FROM User u JOIN u.roles r WHERE r.id = :roleId")
List<User> findByRoleId(@Param("roleId") UUID roleId);
boolean existsByUsername(String username); boolean existsByUsername(String username);
boolean existsByPhone(String phone); boolean existsByPhone(String phone);

View File

@ -0,0 +1,30 @@
package com.ether.pms.auth.scheduler;
import com.ether.pms.auth.service.AuditLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class AuditLogArchiveTask {
private final AuditLogService auditLogService;
/**
* 每天凌晨2点执行归档任务
* 将超过90天的审计日志归档当前实现为删除实际应导出到文件/对象存储
*/
@Scheduled(cron = "0 0 2 * * ?")
public void archiveOldAuditLogs() {
log.info("开始执行审计日志归档任务...");
try {
int archivedCount = auditLogService.archiveOldLogs();
log.info("审计日志归档任务完成,共归档 {} 条记录", archivedCount);
} catch (Exception e) {
log.error("审计日志归档任务失败: {}", e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,138 @@
package com.ether.pms.auth.service;
import com.ether.pms.auth.entity.AuditLog;
import com.ether.pms.auth.repository.AuditLogRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.criteria.Predicate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class AuditLogService {
private final AuditLogRepository auditLogRepository;
private final ObjectMapper objectMapper;
/**
* 异步保存审计日志
*/
@Async("auditLogExecutor")
public void saveAuditLogAsync(AuditLog auditLog) {
try {
auditLogRepository.save(auditLog);
} catch (Exception e) {
log.error("保存审计日志失败: {}", e.getMessage(), e);
}
}
/**
* 同步保存审计日志用于需要立即确认的场景
*/
@Transactional
public AuditLog saveAuditLog(AuditLog auditLog) {
return auditLogRepository.save(auditLog);
}
/**
* 分页查询审计日志最近30天
*/
public Page<AuditLog> findRecentLogs(Pageable pageable) {
LocalDateTime startTime = LocalDateTime.now().minusDays(30);
return auditLogRepository.findRecentLogs(startTime, pageable);
}
/**
* 条件查询审计日志最近30天内
*/
public Page<AuditLog> searchLogs(String module, String action, String username,
LocalDateTime startDate, LocalDateTime endDate,
Pageable pageable) {
// 确保查询范围不超过30天
LocalDateTime maxStartDate = LocalDateTime.now().minusDays(30);
if (startDate == null || startDate.isBefore(maxStartDate)) {
startDate = maxStartDate;
}
if (endDate == null) {
endDate = LocalDateTime.now();
}
final LocalDateTime finalStartDate = startDate;
final LocalDateTime finalEndDate = endDate;
Specification<AuditLog> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 时间范围
predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), finalStartDate));
predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), finalEndDate));
// 模块筛选
if (module != null && !module.isEmpty()) {
predicates.add(cb.equal(root.get("module"), module));
}
// 操作类型筛选
if (action != null && !action.isEmpty()) {
predicates.add(cb.equal(root.get("action"), AuditLog.ActionType.valueOf(action)));
}
// 用户名筛选
if (username != null && !username.isEmpty()) {
predicates.add(cb.like(root.get("username"), "%" + username + "%"));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
return auditLogRepository.findAll(spec, pageable);
}
/**
* 归档超过90天的日志
*/
@Transactional
public int archiveOldLogs() {
LocalDateTime cutoffTime = LocalDateTime.now().minusDays(90);
// 查询需要归档的日志
List<AuditLog> oldLogs = auditLogRepository.findByCreatedAtBefore(cutoffTime);
if (oldLogs.isEmpty()) {
log.info("没有需要归档的审计日志");
return 0;
}
// TODO: 将日志导出到文件或对象存储
// 这里简化处理直接删除
// 实际项目中应该
// 1. 将数据导出为JSON/Parquet文件
// 2. 上传到对象存储MinIO/S3
// 3. 记录归档信息
// 4. 然后删除数据库记录
int deleted = auditLogRepository.deleteByCreatedAtBefore(cutoffTime);
log.info("归档审计日志完成,共归档 {} 条记录", deleted);
return deleted;
}
/**
* 获取最近30天的日志统计
*/
public long getRecentLogCount() {
LocalDateTime startTime = LocalDateTime.now().minusDays(30);
return auditLogRepository.countRecentLogs(startTime);
}
}

View File

@ -2,8 +2,10 @@ package com.ether.pms.auth.service;
import com.ether.pms.auth.entity.Role; import com.ether.pms.auth.entity.Role;
import com.ether.pms.auth.entity.Permission; import com.ether.pms.auth.entity.Permission;
import com.ether.pms.auth.entity.User;
import com.ether.pms.auth.repository.RoleRepository; import com.ether.pms.auth.repository.RoleRepository;
import com.ether.pms.auth.repository.PermissionRepository; import com.ether.pms.auth.repository.PermissionRepository;
import com.ether.pms.auth.repository.UserRepository;
import com.ether.pms.common.BusinessException; import com.ether.pms.common.BusinessException;
import com.ether.pms.common.ErrorCode; import com.ether.pms.common.ErrorCode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -19,6 +21,7 @@ public class RoleService {
private final RoleRepository roleRepository; private final RoleRepository roleRepository;
private final PermissionRepository permissionRepository; private final PermissionRepository permissionRepository;
private final UserRepository userRepository;
public List<Role> findAll() { public List<Role> findAll() {
return roleRepository.findAll(); return roleRepository.findAll();
@ -84,4 +87,14 @@ public class RoleService {
public void delete(UUID id) { public void delete(UUID id) {
roleRepository.deleteById(id); roleRepository.deleteById(id);
} }
public List<Permission> getPermissions(UUID roleId) {
Role role = roleRepository.findWithPermissionsById(roleId)
.orElseThrow(() -> new BusinessException(ErrorCode.ROLE_002));
return role.getPermissions();
}
public List<User> getUsersByRoleId(UUID roleId) {
return userRepository.findByRoleId(roleId);
}
} }

View File

@ -23,7 +23,7 @@ public class UserService {
private final PasswordService passwordService; private final PasswordService passwordService;
public List<User> findAll() { public List<User> findAll() {
return userRepository.findAll(); return userRepository.findAllWithRoles();
} }
public User findById(UUID id) { public User findById(UUID id) {

View File

@ -28,9 +28,14 @@ public enum ErrorCode {
PROJECT_001(5001, "项目编码已存在"), PROJECT_001(5001, "项目编码已存在"),
PROJECT_002(5002, "项目不存在"), PROJECT_002(5002, "项目不存在"),
PROJECT_003(5003, "项目状态转换无效"),
PROJECT_004(5004, "项目成员已存在"),
PROJECT_005(5005, "项目成员不存在"),
PROJECT_006(5006, "项目配置不存在"),
SPACE_001(6001, "空间节点编码已存在"), SPACE_001(6001, "空间节点编码已存在"),
SPACE_002(6002, "空间节点不存在"), SPACE_002(6002, "空间节点不存在"),
SPACE_003(6003, "空间节点存在子节点,无法删除"),
SYSTEM_ERROR(9999, "系统错误"); SYSTEM_ERROR(9999, "系统错误");

View File

@ -23,6 +23,12 @@
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.ether</groupId>
<artifactId>module-auth</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
@ -53,5 +59,18 @@
<groupId>org.mapstruct</groupId> <groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId> <artifactId>mapstruct</artifactId>
</dependency> </dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,9 +1,22 @@
package com.ether.pms.mdm.controller; package com.ether.pms.mdm.controller;
import com.ether.pms.mdm.dto.AddMemberRequest;
import com.ether.pms.mdm.dto.ChangeStatusRequest;
import com.ether.pms.mdm.dto.PageResponse;
import com.ether.pms.mdm.dto.ProjectConfigDTO;
import com.ether.pms.mdm.dto.ProjectMemberDTO;
import com.ether.pms.mdm.dto.ProjectQueryRequest;
import com.ether.pms.mdm.dto.ProjectSelectorItem;
import com.ether.pms.mdm.entity.Project; import com.ether.pms.mdm.entity.Project;
import com.ether.pms.mdm.entity.ProjectStatistics;
import com.ether.pms.mdm.service.ProjectConfigService;
import com.ether.pms.mdm.service.ProjectMemberService;
import com.ether.pms.mdm.service.ProjectService; import com.ether.pms.mdm.service.ProjectService;
import com.ether.pms.mdm.service.ProjectStatisticsService;
import com.ether.pms.common.ApiResponse; import com.ether.pms.common.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -11,40 +24,150 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@RequestMapping("/api/projects") @RequestMapping("/api/mdm/projects")
@RequiredArgsConstructor @RequiredArgsConstructor
public class ProjectController { public class ProjectController {
private final ProjectService projectService; private final ProjectService projectService;
private final ProjectMemberService projectMemberService;
private final ProjectConfigService projectConfigService;
private final ProjectStatisticsService projectStatisticsService;
/**
* PM-001: 分页查询项目列表
*/
@GetMapping @GetMapping
public ResponseEntity<ApiResponse<List<Project>>> findAll() { public ResponseEntity<ApiResponse<PageResponse<Project>>> queryProjects(ProjectQueryRequest request) {
return ResponseEntity.ok(ApiResponse.success(projectService.findAll())); return ResponseEntity.ok(ApiResponse.success(projectService.queryProjects(request)));
} }
/**
* PM-010: 获取项目选择器列表
*/
@GetMapping("/selector")
public ResponseEntity<ApiResponse<List<ProjectSelectorItem>>> getSelectorList() {
return ResponseEntity.ok(ApiResponse.success(projectService.getSelectorList()));
}
/**
* PM-005: 生成项目编码
*/
@GetMapping("/generate-code")
public ResponseEntity<ApiResponse<String>> generateCode() {
return ResponseEntity.ok(ApiResponse.success(projectService.generateCode()));
}
@GetMapping("/{id}") @GetMapping("/{id}")
public ResponseEntity<ApiResponse<Project>> findById(@PathVariable UUID id) { public ResponseEntity<ApiResponse<Project>> findById(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(projectService.findById(id))); return ResponseEntity.ok(ApiResponse.success(projectService.findById(id)));
} }
@GetMapping("/code/{code}") @GetMapping("/code/{code}")
public ResponseEntity<ApiResponse<Project>> findByCode(@PathVariable String code) { public ResponseEntity<ApiResponse<Project>> findByCode(@PathVariable String code) {
return ResponseEntity.ok(ApiResponse.success(projectService.findByCode(code))); return ResponseEntity.ok(ApiResponse.success(projectService.findByCode(code)));
} }
@PostMapping @PostMapping
public ResponseEntity<ApiResponse<Project>> create(@RequestBody Project project) { public ResponseEntity<ApiResponse<Project>> create(@RequestBody Project project) {
return ResponseEntity.ok(ApiResponse.success(projectService.create(project))); return ResponseEntity.ok(ApiResponse.success(projectService.create(project)));
} }
@PutMapping("/{id}") @PutMapping("/{id}")
public ResponseEntity<ApiResponse<Project>> update(@PathVariable UUID id, @RequestBody Project project) { public ResponseEntity<ApiResponse<Project>> update(@PathVariable UUID id, @RequestBody Project project) {
return ResponseEntity.ok(ApiResponse.success(projectService.update(id, project))); return ResponseEntity.ok(ApiResponse.success(projectService.update(id, project)));
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) { public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) {
projectService.delete(id); projectService.delete(id);
return ResponseEntity.ok(ApiResponse.success()); return ResponseEntity.ok(ApiResponse.success());
} }
// ========================================
// PM-003: 成员管理
// ========================================
/**
* PM-003: 获取项目成员列表
*/
@GetMapping("/{id}/members")
public ResponseEntity<ApiResponse<PageResponse<ProjectMemberDTO>>> getMembers(
@PathVariable("id") UUID projectId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(ApiResponse.success(
projectMemberService.getMembers(projectId, PageRequest.of(page, size))
));
}
/**
* PM-003: 添加项目成员
*/
@PostMapping("/{id}/members")
public ResponseEntity<ApiResponse<Void>> addMembers(
@PathVariable("id") UUID projectId,
@Valid @RequestBody AddMemberRequest request) {
projectMemberService.addMembers(projectId, request);
return ResponseEntity.ok(ApiResponse.success());
}
/**
* PM-003: 移除项目成员
*/
@DeleteMapping("/{id}/members/{memberId}")
public ResponseEntity<ApiResponse<Void>> removeMember(
@PathVariable("id") UUID projectId,
@PathVariable UUID memberId) {
projectMemberService.removeMember(projectId, memberId);
return ResponseEntity.ok(ApiResponse.success());
}
// ========================================
// PM-002: 统计数据
// ========================================
/**
* PM-002: 获取项目统计数据
*/
@GetMapping("/{id}/statistics")
public ResponseEntity<ApiResponse<ProjectStatistics>> getStatistics(@PathVariable("id") UUID projectId) {
return ResponseEntity.ok(ApiResponse.success(projectStatisticsService.getStatistics(projectId)));
}
// ========================================
// PM-006: 状态管理
// ========================================
/**
* PM-006: 变更项目状态
*/
@PutMapping("/{id}/status")
public ResponseEntity<ApiResponse<Void>> changeStatus(
@PathVariable UUID id,
@Valid @RequestBody ChangeStatusRequest request) {
projectService.changeStatus(id, request.getStatus(), request.getReason());
return ResponseEntity.ok(ApiResponse.success());
}
// ========================================
// PM-008: 配置管理
// ========================================
/**
* PM-008: 获取项目配置
*/
@GetMapping("/{id}/config")
public ResponseEntity<ApiResponse<ProjectConfigDTO>> getConfig(@PathVariable("id") UUID projectId) {
return ResponseEntity.ok(ApiResponse.success(projectConfigService.getConfig(projectId)));
}
/**
* PM-008: 更新项目配置
*/
@PutMapping("/{id}/config")
public ResponseEntity<ApiResponse<ProjectConfigDTO>> updateConfig(
@PathVariable("id") UUID projectId,
@RequestBody ProjectConfigDTO dto) {
return ResponseEntity.ok(ApiResponse.success(projectConfigService.updateConfig(projectId, dto)));
}
} }

View File

@ -1,8 +1,12 @@
package com.ether.pms.mdm.controller; package com.ether.pms.mdm.controller;
import com.ether.pms.common.ApiResponse;
import com.ether.pms.mdm.dto.SpaceNodeCreateDTO;
import com.ether.pms.mdm.dto.SpaceNodeTreeDTO;
import com.ether.pms.mdm.dto.SpaceNodeUpdateDTO;
import com.ether.pms.mdm.entity.SpaceNode; import com.ether.pms.mdm.entity.SpaceNode;
import com.ether.pms.mdm.service.SpaceNodeService; import com.ether.pms.mdm.service.SpaceNodeService;
import com.ether.pms.common.ApiResponse; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -11,49 +15,64 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@RequestMapping("/api/space-nodes") @RequestMapping("/api/v1/mdm/space-nodes")
@RequiredArgsConstructor @RequiredArgsConstructor
public class SpaceNodeController { public class SpaceNodeController {
private final SpaceNodeService spaceNodeService; private final SpaceNodeService spaceNodeService;
@GetMapping @GetMapping
public ResponseEntity<ApiResponse<List<SpaceNode>>> findAll() { public ResponseEntity<ApiResponse<List<SpaceNode>>> findAll() {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findAll())); return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findAll()));
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public ResponseEntity<ApiResponse<SpaceNode>> findById(@PathVariable UUID id) { public ResponseEntity<ApiResponse<SpaceNode>> findById(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findById(id))); return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findById(id)));
} }
@GetMapping("/project/{projectCode}") @GetMapping("/project/{projectId}")
public ResponseEntity<ApiResponse<List<SpaceNode>>> findByProject(@PathVariable String projectCode) { public ResponseEntity<ApiResponse<List<SpaceNode>>> findByProject(@PathVariable UUID projectId) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByProject(projectCode))); return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByProjectId(projectId)));
} }
@GetMapping("/project/{projectCode}/type/{nodeType}") @GetMapping("/project/{projectId}/tree")
public ResponseEntity<ApiResponse<List<SpaceNodeTreeDTO>>> getTree(@PathVariable UUID projectId) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.getTreeByProjectId(projectId)));
}
@GetMapping("/project/{projectId}/roots")
public ResponseEntity<ApiResponse<List<SpaceNode>>> getRoots(@PathVariable UUID projectId) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findRootsByProjectId(projectId)));
}
@GetMapping("/project/{projectId}/type/{nodeType}")
public ResponseEntity<ApiResponse<List<SpaceNode>>> findByProjectAndType( public ResponseEntity<ApiResponse<List<SpaceNode>>> findByProjectAndType(
@PathVariable String projectCode, @PathVariable UUID projectId,
@PathVariable String nodeType) { @PathVariable SpaceNode.NodeType nodeType) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByProjectAndType(projectCode, nodeType))); return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByProjectIdAndNodeType(projectId, nodeType)));
} }
@GetMapping("/parent/{parentId}/children")
public ResponseEntity<ApiResponse<List<SpaceNode>>> getChildren(@PathVariable UUID parentId) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findChildren(parentId)));
}
@GetMapping("/parent/{parentCode}") @GetMapping("/parent/{parentCode}")
public ResponseEntity<ApiResponse<List<SpaceNode>>> findByParent(@PathVariable String parentCode) { public ResponseEntity<ApiResponse<List<SpaceNode>>> findByParent(@PathVariable String parentCode) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByParent(parentCode))); return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByParentCode(parentCode)));
} }
@PostMapping @PostMapping
public ResponseEntity<ApiResponse<SpaceNode>> create(@RequestBody SpaceNode spaceNode) { public ResponseEntity<ApiResponse<SpaceNode>> create(@Valid @RequestBody SpaceNodeCreateDTO dto) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.create(spaceNode))); return ResponseEntity.ok(ApiResponse.success(spaceNodeService.create(dto)));
} }
@PutMapping("/{id}") @PutMapping("/{id}")
public ResponseEntity<ApiResponse<SpaceNode>> update(@PathVariable UUID id, @RequestBody SpaceNode spaceNode) { public ResponseEntity<ApiResponse<SpaceNode>> update(@PathVariable UUID id, @RequestBody SpaceNodeUpdateDTO dto) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.update(id, spaceNode))); return ResponseEntity.ok(ApiResponse.success(spaceNodeService.update(id, dto)));
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) { public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) {
spaceNodeService.delete(id); spaceNodeService.delete(id);

View File

@ -0,0 +1,31 @@
package com.ether.pms.mdm.dto;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.UUID;
/**
* 添加项目成员请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AddMemberRequest {
/**
* 用户ID列表
*/
@NotEmpty(message = "用户ID列表不能为空")
private List<UUID> userIds;
/**
* 项目中的角色
*/
@NotNull(message = "项目角色不能为空")
private String roleInProject;
}

View File

@ -0,0 +1,27 @@
package com.ether.pms.mdm.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 项目状态变更请求
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChangeStatusRequest {
/**
* 新状态
*/
@NotBlank(message = "状态不能为空")
private String status;
/**
* 变更原因
*/
private String reason;
}

View File

@ -0,0 +1,43 @@
package com.ether.pms.mdm.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 分页响应DTO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResponse<T> {
private List<T> content;
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean first;
private boolean last;
public static <T> PageResponse<T> of(List<T> content, int page, int size, long totalElements) {
int totalPages = (int) Math.ceil((double) totalElements / size);
return new PageResponse<>(
content,
page,
size,
totalElements,
totalPages,
page == 0,
page >= totalPages - 1
);
}
}

View File

@ -0,0 +1,70 @@
package com.ether.pms.mdm.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
/**
* 项目配置DTO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProjectConfigDTO {
private UUID id;
private UUID projectId;
/**
* 启用预约功能
*/
private Boolean enableReservation;
/**
* 启用访客功能
*/
private Boolean enableVisitor;
/**
* 启用投诉功能
*/
private Boolean enableComplaint;
/**
* 启用缴费功能
*/
private Boolean enablePayment;
/**
* 启用公告功能
*/
private Boolean enableAnnouncement;
/**
* 启用问卷功能
*/
private Boolean enableSurvey;
/**
* 启用投票功能
*/
private Boolean enableVote;
/**
* 启用报修功能
*/
private Boolean enableMaintenance;
/**
* 启用资产功能
*/
private Boolean enableAsset;
/**
* 自定义配置JSON格式
*/
private String customConfig;
}

View File

@ -0,0 +1,57 @@
package com.ether.pms.mdm.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 项目成员DTO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProjectMemberDTO {
/**
* 成员关联ID
*/
private UUID id;
/**
* 用户ID
*/
private UUID userId;
/**
* 用户名
*/
private String username;
/**
* 真实姓名
*/
private String realName;
/**
* 手机号
*/
private String phone;
/**
* 头像
*/
private String avatar;
/**
* 项目中的角色
*/
private String roleInProject;
/**
* 加入时间
*/
private LocalDateTime joinedAt;
}

View File

@ -0,0 +1,22 @@
package com.ether.pms.mdm.dto;
import lombok.Data;
/**
* 项目查询请求DTO
*/
@Data
public class ProjectQueryRequest {
private String keyword;
private String status;
private Integer page = 0;
private Integer size = 20;
private String sortBy = "createdAt";
private String sortDirection = "DESC";
}

View File

@ -0,0 +1,28 @@
package com.ether.pms.mdm.dto;
import lombok.Data;
/**
* 项目选择器项DTO
*/
@Data
public class ProjectSelectorItem {
private String id;
private String code;
private String name;
private String status;
public ProjectSelectorItem() {
}
public ProjectSelectorItem(String id, String code, String name, String status) {
this.id = id;
this.code = code;
this.name = name;
this.status = status;
}
}

View File

@ -0,0 +1,71 @@
package com.ether.pms.mdm.dto;
import com.ether.pms.mdm.entity.SpaceNode;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.util.UUID;
@Data
public class SpaceNodeCreateDTO {
@NotNull(message = "项目ID不能为空")
private UUID projectId;
@NotBlank(message = "空间节点代码不能为空")
private String code;
@NotBlank(message = "空间节点名称不能为空")
private String name;
private String fullName;
private String shortName;
@NotNull(message = "节点大类不能为空")
private SpaceNode.NodeCategory nodeCategory;
@NotNull(message = "空间节点类型不能为空")
private SpaceNode.NodeType nodeType;
private String usageType;
private UUID parentId;
private Integer sortOrder = 0;
private String status = "ACTIVE";
private String deliveryStatus;
private String decorationStatus;
private BigDecimal buildingArea;
private BigDecimal usableArea;
private BigDecimal sharedArea;
private BigDecimal landArea;
private BigDecimal longitude;
private BigDecimal latitude;
private BigDecimal altitude;
private Integer floorNumber;
private String province;
private String city;
private String district;
private String street;
private String address;
private String attributes;
}

View File

@ -0,0 +1,55 @@
package com.ether.pms.mdm.dto;
import com.ether.pms.mdm.entity.SpaceNode;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Data
public class SpaceNodeTreeDTO {
private UUID id;
private String code;
private String name;
private String fullName;
private SpaceNode.NodeCategory nodeCategory;
private SpaceNode.NodeType nodeType;
private String nodeTypeName;
private UUID parentId;
private Integer level;
private Integer sortOrder;
private String status;
private BigDecimal buildingArea;
private BigDecimal usableArea;
private Integer floorNumber;
private String address;
private List<SpaceNodeTreeDTO> children = new ArrayList<>();
public static SpaceNodeTreeDTO fromEntity(SpaceNode node) {
SpaceNodeTreeDTO dto = new SpaceNodeTreeDTO();
dto.setId(node.getId());
dto.setCode(node.getCode());
dto.setName(node.getName());
dto.setFullName(node.getFullName());
dto.setNodeCategory(node.getNodeCategory());
dto.setNodeType(node.getNodeType());
dto.setNodeTypeName(node.getNodeType() != null ? node.getNodeType().getDesc() : null);
dto.setParentId(node.getParentId());
dto.setLevel(node.getLevel());
dto.setSortOrder(node.getSortOrder());
dto.setStatus(node.getStatus());
dto.setBuildingArea(node.getBuildingArea());
dto.setUsableArea(node.getUsableArea());
dto.setFloorNumber(node.getFloorNumber());
dto.setAddress(node.getAddress());
return dto;
}
public void addChild(SpaceNodeTreeDTO child) {
if (this.children == null) {
this.children = new ArrayList<>();
}
this.children.add(child);
}
}

View File

@ -0,0 +1,57 @@
package com.ether.pms.mdm.dto;
import com.ether.pms.mdm.entity.SpaceNode;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class SpaceNodeUpdateDTO {
private String name;
private String fullName;
private String shortName;
private SpaceNode.NodeCategory nodeCategory;
private SpaceNode.NodeType nodeType;
private String usageType;
private Integer sortOrder = 0;
private String status;
private String deliveryStatus;
private String decorationStatus;
private BigDecimal buildingArea;
private BigDecimal usableArea;
private BigDecimal sharedArea;
private BigDecimal landArea;
private BigDecimal longitude;
private BigDecimal latitude;
private BigDecimal altitude;
private Integer floorNumber;
private String province;
private String city;
private String district;
private String street;
private String address;
private String attributes;
}

View File

@ -36,9 +36,25 @@ public class Project {
@Column(length = 100) @Column(length = 100)
private String address; private String address;
@Size(max = 20, message = "项目类型长度不能超过20位") @Column(name = "project_type", length = 20)
@Column(length = 20) @Enumerated(EnumType.STRING)
private String projectType; private ProjectType projectType = ProjectType.RESIDENTIAL;
public enum ProjectType {
RESIDENTIAL("住宅"),
OFFICE("办公"),
INDUSTRIAL_PARK("产业园区");
private final String desc;
ProjectType(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
@Column(length = 50) @Column(length = 50)
private String province; private String province;

View File

@ -0,0 +1,69 @@
package com.ether.pms.mdm.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 项目配置实体
*/
@Entity
@Table(name = "mdm_project_config")
@Data
public class ProjectConfig {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "project_id", nullable = false, unique = true)
private UUID projectId;
@Column(name = "enable_reservation")
private Boolean enableReservation = false;
@Column(name = "enable_visitor")
private Boolean enableVisitor = false;
@Column(name = "enable_complaint")
private Boolean enableComplaint = true;
@Column(name = "enable_payment")
private Boolean enablePayment = false;
@Column(name = "enable_announcement")
private Boolean enableAnnouncement = true;
@Column(name = "enable_survey")
private Boolean enableSurvey = false;
@Column(name = "enable_vote")
private Boolean enableVote = false;
@Column(name = "enable_maintenance")
private Boolean enableMaintenance = true;
@Column(name = "enable_asset")
private Boolean enableAsset = false;
@Column(name = "custom_config", columnDefinition = "TEXT")
private String customConfig;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
}

View File

@ -0,0 +1,78 @@
package com.ether.pms.mdm.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 项目统计信息实体
*/
@Entity
@Table(name = "mdm_project_statistics")
@Data
public class ProjectStatistics {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "project_id", nullable = false, unique = true)
private UUID projectId;
@Column(name = "member_count")
private Integer memberCount = 0;
@Column(name = "building_count")
private Integer buildingCount = 0;
@Column(name = "unit_count")
private Integer unitCount = 0;
@Column(name = "room_count")
private Integer roomCount = 0;
@Column(name = "owner_count")
private Integer ownerCount = 0;
@Column(name = "tenant_count")
private Integer tenantCount = 0;
@Column(name = "last_synced_at")
private LocalDateTime lastSyncedAt;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
if (this.memberCount == null) {
this.memberCount = 0;
}
if (this.buildingCount == null) {
this.buildingCount = 0;
}
if (this.unitCount == null) {
this.unitCount = 0;
}
if (this.roomCount == null) {
this.roomCount = 0;
}
if (this.ownerCount == null) {
this.ownerCount = 0;
}
if (this.tenantCount == null) {
this.tenantCount = 0;
}
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
}

View File

@ -0,0 +1,45 @@
package com.ether.pms.mdm.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 项目状态变更历史实体
*/
@Entity
@Table(name = "mdm_project_status_history")
@Data
public class ProjectStatusHistory {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(name = "project_id", nullable = false)
private UUID projectId;
@Column(name = "from_status", length = 20)
private String fromStatus;
@Column(name = "to_status", length = 20, nullable = false)
private String toStatus;
@Column(name = "reason", length = 500)
private String reason;
@Column(name = "operator_id")
private UUID operatorId;
@Column(name = "operator_name", length = 50)
private String operatorName;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
}
}

View File

@ -5,66 +5,208 @@ import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.UUID; import java.util.UUID;
@Entity @Entity
@Table(name = "mdm_space_node") @Table(name = "mdm_space_node", indexes = {
@Index(name = "idx_space_node_project", columnList = "project_id"),
@Index(name = "idx_space_node_parent", columnList = "parent_id"),
@Index(name = "idx_space_node_type", columnList = "node_type"),
@Index(name = "idx_space_node_tree_path", columnList = "tree_path")
})
@Data @Data
public class SpaceNode { public class SpaceNode {
@Id @Id
@GeneratedValue(strategy = GenerationType.UUID) @GeneratedValue(strategy = GenerationType.UUID)
private UUID id; private UUID id;
@NotNull(message = "项目ID不能为空")
@Column(name = "project_id", nullable = false)
private UUID projectId;
@NotNull(message = "空间节点代码不能为空") @NotNull(message = "空间节点代码不能为空")
@Size(min = 2, max = 50, message = "空间节点代码长度必须在2-50位之间") @Size(min = 2, max = 50, message = "空间节点代码长度必须在2-50位之间")
@Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "空间节点代码只能包含字母、数字、连字符和下划线") @Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "空间节点代码只能包含字母、数字、连字符和下划线")
@Column(nullable = false, length = 50) @Column(nullable = false, length = 50)
private String code; private String code;
@NotNull(message = "空间节点名称不能为空") @NotNull(message = "空间节点名称不能为空")
@Size(min = 2, max = 100, message = "空间节点名称长度必须在2-100位之间") @Size(min = 2, max = 100, message = "空间节点名称长度必须在2-100位之间")
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
private String name; private String name;
@Column(length = 500)
private String fullName;
@Column(length = 50)
private String shortName;
@NotNull(message = "节点大类不能为空")
@Column(name = "node_category", nullable = false, length = 20)
@Enumerated(EnumType.STRING)
private NodeCategory nodeCategory;
@NotNull(message = "空间节点类型不能为空") @NotNull(message = "空间节点类型不能为空")
@Size(max = 50, message = "空间节点类型长度不能超过50位") @Column(name = "node_type", nullable = false, length = 30)
@Column(nullable = false, length = 50) @Enumerated(EnumType.STRING)
private String nodeType; private NodeType nodeType;
@Size(max = 50, message = "父节点代码长度不能超过50位") @Column(length = 30)
@Column(length = 50) private String usageType;
@Column(name = "parent_id")
private UUID parentId;
@Column(name = "parent_code", length = 50)
private String parentCode; private String parentCode;
@NotNull(message = "项目代码不能为空") @Column(name = "tree_path", length = 1000)
@Size(max = 50, message = "项目代码长度不能超过50位") private String treePath;
@Column(nullable = false)
private String projectCode; @Column(name = "tree_path_name", length = 1000)
private String treePathName;
private Integer sortOrder;
@Column(name = "level")
@Column(length = 50) private Integer level = 0;
private String building;
@Column(name = "sort_order")
@Column(length = 50) private Integer sortOrder = 0;
private String unit;
@Column(length = 50)
private String floor;
@Column(length = 50)
private String roomNumber;
private Integer area;
@Column(length = 20) @Column(length = 20)
private String status; private String status = "ACTIVE";
@Column(name = "delivery_status", length = 20)
private String deliveryStatus;
@Column(name = "decoration_status", length = 20)
private String decorationStatus;
@Column(name = "building_area", precision = 10, scale = 2)
private BigDecimal buildingArea;
@Column(name = "usable_area", precision = 10, scale = 2)
private BigDecimal usableArea;
@Column(name = "shared_area", precision = 10, scale = 2)
private BigDecimal sharedArea;
@Column(name = "land_area", precision = 10, scale = 2)
private BigDecimal landArea;
@Column(precision = 10, scale = 6)
private BigDecimal longitude;
@Column(precision = 10, scale = 6)
private BigDecimal latitude;
@Column(precision = 8, scale = 2)
private BigDecimal altitude;
@Column(name = "floor_number")
private Integer floorNumber;
@Column(length = 50)
private String province;
@Column(length = 50)
private String city;
@Column(length = 50)
private String district;
@Column(length = 100)
private String street;
@Column(length = 255)
private String address;
@Column(name = "attributes", columnDefinition = "TEXT")
private String attributes;
@Column(name = "created_at")
private LocalDateTime createdAt; private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
@Column(name = "created_by")
private UUID createdBy;
@Column(name = "updated_by")
private UUID updatedBy;
@Column(name = "is_deleted")
private Boolean isDeleted = false;
// ========== 设备扩展字段 ==========
@Column(name = "is_equipment")
private Boolean isEquipment = false;
@Column(name = "design_life_years")
private Integer designLifeYears;
@Column(name = "rated_power", precision = 10, scale = 2)
private BigDecimal ratedPower;
@Column(name = "rated_voltage", length = 20)
private String ratedVoltage;
@Column(name = "rated_current", precision = 10, scale = 2)
private BigDecimal ratedCurrent;
@Column(name = "maintenance_vendor", length = 100)
private String maintenanceVendor;
@Column(name = "maintenance_vendor_contact", length = 50)
private String maintenanceVendorContact;
@Column(name = "maintenance_vendor_phone", length = 20)
private String maintenanceVendorPhone;
@Column(name = "maintenance_contract_no", length = 50)
private String maintenanceContractNo;
@Column(name = "maintenance_contract_start")
private LocalDate maintenanceContractStart;
@Column(name = "maintenance_contract_end")
private LocalDate maintenanceContractEnd;
@Column(name = "special_equipment_type", length = 50)
private String specialEquipmentType;
@Column(name = "special_equipment_cert", length = 100)
private String specialEquipmentCert;
@Column(name = "inspection_cycle")
private Integer inspectionCycle;
@Column(name = "next_inspection_date")
private LocalDate nextInspectionDate;
@Column(name = "last_inspection_date")
private LocalDate lastInspectionDate;
@Column(name = "last_inspection_result", length = 20)
private String lastInspectionResult;
@Column(name = "common_spare_parts", columnDefinition = "TEXT")
private String commonSpareParts;
@Column(name = "energy_consumption_standard", precision = 12, scale = 2)
private BigDecimal energyConsumptionStandard;
@Column(name = "installation_environment", length = 50)
private String installationEnvironment;
@Column(name = "protection_level", length = 20)
private String protectionLevel;
// ========== 设备扩展字段结束 ==========
@PrePersist @PrePersist
public void prePersist() { public void prePersist() {
this.createdAt = LocalDateTime.now(); this.createdAt = LocalDateTime.now();
@ -75,27 +217,72 @@ public class SpaceNode {
if (this.sortOrder == null) { if (this.sortOrder == null) {
this.sortOrder = 0; this.sortOrder = 0;
} }
if (this.level == null) {
this.level = 0;
}
if (this.isDeleted == null) {
this.isDeleted = false;
}
} }
@PreUpdate @PreUpdate
public void preUpdate() { public void preUpdate() {
this.updatedAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now();
} }
public enum NodeType { public enum NodeCategory {
PROJECT("项目"), BUILDING("建筑空间"),
PHASE(""), PARKING("停车空间"),
BUILDING("楼栋"), FACILITY("设施空间"),
UNIT("单元"), AREA("区域空间");
FLOOR("楼层"),
ROOM("房间"),
PARKING("车位"),
STORE("商铺");
private final String desc; private final String desc;
NodeType(String desc) { NodeCategory(String desc) {
this.desc = desc; this.desc = desc;
} }
public String getDesc() {
return desc;
}
}
public enum NodeType {
BUILDING("楼栋", NodeCategory.BUILDING, 1),
UNIT("单元", NodeCategory.BUILDING, 2),
FLOOR("楼层", NodeCategory.BUILDING, 3),
ROOM("房间", NodeCategory.BUILDING, 4),
SHOP("商铺", NodeCategory.BUILDING, 2),
GARAGE("车库", NodeCategory.PARKING, 1),
PARKING_AREA("停车区域", NodeCategory.PARKING, 2),
PARKING_SPACE("车位", NodeCategory.PARKING, 3),
EQUIPMENT_ROOM("设备房", NodeCategory.FACILITY, 1),
PROPERTY_OFFICE("物业用房", NodeCategory.FACILITY, 1),
SECURITY_ROOM("门岗", NodeCategory.FACILITY, 1),
PUBLIC_AREA("公共区域", NodeCategory.AREA, 1),
GREEN_AREA("绿化区域", NodeCategory.AREA, 1),
ROAD("道路", NodeCategory.AREA, 1);
private final String desc;
private final NodeCategory category;
private final int order;
NodeType(String desc, NodeCategory category, int order) {
this.desc = desc;
this.category = category;
this.order = order;
}
public String getDesc() {
return desc;
}
public NodeCategory getCategory() {
return category;
}
public int getOrder() {
return order;
}
} }
} }

View File

@ -0,0 +1,59 @@
package com.ether.pms.mdm.enums;
import lombok.Getter;
/**
* 项目状态枚举
*/
@Getter
public enum ProjectStatus {
DRAFT("DRAFT", "草稿"),
ACTIVE("ACTIVE", "启用"),
INACTIVE("INACTIVE", "停用"),
ARCHIVED("ARCHIVED", "归档");
private final String code;
private final String description;
ProjectStatus(String code, String description) {
this.code = code;
this.description = description;
}
/**
* 判断是否可以转换到目标状态
* 状态转换规则
* - DRAFT -> ACTIVE
* - ACTIVE -> INACTIVE, ARCHIVED
* - INACTIVE -> ACTIVE, ARCHIVED
* - ARCHIVED -> ACTIVE (需要特殊权限)
*/
public boolean canTransitionTo(ProjectStatus target) {
if (target == null) {
return false;
}
return switch (this) {
case DRAFT -> target == ACTIVE;
case ACTIVE -> target == INACTIVE || target == ARCHIVED;
case INACTIVE -> target == ACTIVE || target == ARCHIVED;
case ARCHIVED -> target == ACTIVE;
};
}
/**
* 根据code获取枚举
*/
public static ProjectStatus fromCode(String code) {
if (code == null) {
return null;
}
for (ProjectStatus status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return null;
}
}

View File

@ -0,0 +1,18 @@
package com.ether.pms.mdm.repository;
import com.ether.pms.mdm.entity.ProjectConfig;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface ProjectConfigRepository extends JpaRepository<ProjectConfig, UUID> {
Optional<ProjectConfig> findByProjectId(UUID projectId);
void deleteByProjectId(UUID projectId);
boolean existsByProjectId(UUID projectId);
}

View File

@ -1,15 +1,40 @@
package com.ether.pms.mdm.repository; package com.ether.pms.mdm.repository;
import com.ether.pms.mdm.entity.Project; import com.ether.pms.mdm.entity.Project;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Repository @Repository
public interface ProjectRepository extends JpaRepository<Project, UUID> { public interface ProjectRepository extends JpaRepository<Project, UUID>, JpaSpecificationExecutor<Project> {
Optional<Project> findByCode(String code); Optional<Project> findByCode(String code);
boolean existsByCode(String code); boolean existsByCode(String code);
Page<Project> findByStatus(String status, Pageable pageable);
@Query("SELECT p FROM Project p WHERE " +
"(:keyword IS NULL OR p.name LIKE %:keyword% " +
"OR p.code LIKE %:keyword%) " +
"AND (:status IS NULL OR p.status = :status)")
Page<Project> searchProjects(@Param("keyword") String keyword,
@Param("status") String status,
Pageable pageable);
@Query("SELECT p FROM Project p WHERE p.status IN ('ACTIVE', 'INACTIVE') ORDER BY p.name")
List<Project> findActiveProjectsForSelector();
@Query("SELECT COUNT(p) FROM Project p WHERE p.status = :status")
long countByStatus(@Param("status") String status);
boolean existsByCodeAndIdNot(String code, UUID id);
} }

View File

@ -0,0 +1,18 @@
package com.ether.pms.mdm.repository;
import com.ether.pms.mdm.entity.ProjectStatistics;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface ProjectStatisticsRepository extends JpaRepository<ProjectStatistics, UUID> {
Optional<ProjectStatistics> findByProjectId(UUID projectId);
void deleteByProjectId(UUID projectId);
boolean existsByProjectId(UUID projectId);
}

View File

@ -0,0 +1,26 @@
package com.ether.pms.mdm.repository;
import com.ether.pms.mdm.entity.ProjectStatusHistory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface ProjectStatusHistoryRepository extends JpaRepository<ProjectStatusHistory, UUID> {
List<ProjectStatusHistory> findByProjectIdOrderByCreatedAtDesc(UUID projectId);
Page<ProjectStatusHistory> findByProjectIdOrderByCreatedAtDesc(UUID projectId, Pageable pageable);
@Query("SELECT h FROM ProjectStatusHistory h WHERE h.projectId = :projectId ORDER BY h.createdAt DESC LIMIT 1")
Optional<ProjectStatusHistory> findLatestByProjectId(@Param("projectId") UUID projectId);
long countByProjectId(UUID projectId);
}

View File

@ -5,16 +5,25 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Repository @Repository
public interface SpaceNodeRepository extends JpaRepository<SpaceNode, UUID> { public interface SpaceNodeRepository extends JpaRepository<SpaceNode, UUID> {
List<SpaceNode> findByProjectCode(String projectCode); boolean existsByProjectIdAndCode(UUID projectId, String code);
List<SpaceNode> findByProjectCodeAndNodeType(String projectCode, String nodeType); List<SpaceNode> findByIsDeletedFalse();
List<SpaceNode> findByParentCode(String parentCode); Optional<SpaceNode> findByIdAndIsDeletedFalse(UUID id);
boolean existsByCode(String code); List<SpaceNode> findByProjectIdAndIsDeletedFalseOrderBySortOrderAsc(UUID projectId);
List<SpaceNode> findByProjectIdAndNodeTypeAndIsDeletedFalse(UUID projectId, SpaceNode.NodeType nodeType);
List<SpaceNode> findByProjectIdAndParentIdIsNullAndIsDeletedFalse(UUID projectId);
List<SpaceNode> findByParentIdAndIsDeletedFalseOrderBySortOrderAsc(UUID parentId);
List<SpaceNode> findByParentCodeAndIsDeletedFalse(String parentCode);
} }

View File

@ -0,0 +1,130 @@
package com.ether.pms.mdm.service;
import com.ether.pms.mdm.dto.ProjectConfigDTO;
import com.ether.pms.mdm.entity.ProjectConfig;
import com.ether.pms.mdm.repository.ProjectConfigRepository;
import com.ether.pms.mdm.repository.ProjectRepository;
import com.ether.pms.common.BusinessException;
import com.ether.pms.common.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
/**
* 项目配置管理服务
*/
@Service
@RequiredArgsConstructor
public class ProjectConfigService {
private final ProjectConfigRepository projectConfigRepository;
private final ProjectRepository projectRepository;
/**
* PM-008: 获取项目配置
*/
public ProjectConfigDTO getConfig(UUID projectId) {
// 验证项目存在
if (!projectRepository.existsById(projectId)) {
throw new BusinessException(ErrorCode.PROJECT_002);
}
ProjectConfig config = projectConfigRepository.findByProjectId(projectId)
.orElseGet(() -> createDefaultConfig(projectId));
return toDTO(config);
}
/**
* PM-008: 更新项目配置
*/
@Transactional
public ProjectConfigDTO updateConfig(UUID projectId, ProjectConfigDTO dto) {
// 验证项目存在
if (!projectRepository.existsById(projectId)) {
throw new BusinessException(ErrorCode.PROJECT_002);
}
ProjectConfig config = projectConfigRepository.findByProjectId(projectId)
.orElseGet(() -> {
ProjectConfig newConfig = new ProjectConfig();
newConfig.setProjectId(projectId);
return newConfig;
});
// 更新配置
if (dto.getEnableReservation() != null) {
config.setEnableReservation(dto.getEnableReservation());
}
if (dto.getEnableVisitor() != null) {
config.setEnableVisitor(dto.getEnableVisitor());
}
if (dto.getEnableComplaint() != null) {
config.setEnableComplaint(dto.getEnableComplaint());
}
if (dto.getEnablePayment() != null) {
config.setEnablePayment(dto.getEnablePayment());
}
if (dto.getEnableAnnouncement() != null) {
config.setEnableAnnouncement(dto.getEnableAnnouncement());
}
if (dto.getEnableSurvey() != null) {
config.setEnableSurvey(dto.getEnableSurvey());
}
if (dto.getEnableVote() != null) {
config.setEnableVote(dto.getEnableVote());
}
if (dto.getEnableMaintenance() != null) {
config.setEnableMaintenance(dto.getEnableMaintenance());
}
if (dto.getEnableAsset() != null) {
config.setEnableAsset(dto.getEnableAsset());
}
if (dto.getCustomConfig() != null) {
config.setCustomConfig(dto.getCustomConfig());
}
ProjectConfig saved = projectConfigRepository.save(config);
return toDTO(saved);
}
/**
* 创建默认配置
*/
private ProjectConfig createDefaultConfig(UUID projectId) {
ProjectConfig config = new ProjectConfig();
config.setProjectId(projectId);
config.setEnableReservation(false);
config.setEnableVisitor(false);
config.setEnableComplaint(true);
config.setEnablePayment(false);
config.setEnableAnnouncement(true);
config.setEnableSurvey(false);
config.setEnableVote(false);
config.setEnableMaintenance(true);
config.setEnableAsset(false);
return projectConfigRepository.save(config);
}
/**
* 转换为DTO
*/
private ProjectConfigDTO toDTO(ProjectConfig config) {
ProjectConfigDTO dto = new ProjectConfigDTO();
dto.setId(config.getId());
dto.setProjectId(config.getProjectId());
dto.setEnableReservation(config.getEnableReservation());
dto.setEnableVisitor(config.getEnableVisitor());
dto.setEnableComplaint(config.getEnableComplaint());
dto.setEnablePayment(config.getEnablePayment());
dto.setEnableAnnouncement(config.getEnableAnnouncement());
dto.setEnableSurvey(config.getEnableSurvey());
dto.setEnableVote(config.getEnableVote());
dto.setEnableMaintenance(config.getEnableMaintenance());
dto.setEnableAsset(config.getEnableAsset());
dto.setCustomConfig(config.getCustomConfig());
return dto;
}
}

View File

@ -0,0 +1,138 @@
package com.ether.pms.mdm.service;
import com.ether.pms.auth.entity.User;
import com.ether.pms.auth.entity.UserProject;
import com.ether.pms.auth.repository.UserProjectRepository;
import com.ether.pms.auth.repository.UserRepository;
import com.ether.pms.mdm.dto.AddMemberRequest;
import com.ether.pms.mdm.dto.PageResponse;
import com.ether.pms.mdm.dto.ProjectMemberDTO;
import com.ether.pms.mdm.repository.ProjectRepository;
import com.ether.pms.common.BusinessException;
import com.ether.pms.common.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 项目成员管理服务
*/
@Service
@RequiredArgsConstructor
public class ProjectMemberService {
private final UserProjectRepository userProjectRepository;
private final UserRepository userRepository;
private final ProjectRepository projectRepository;
/**
* PM-003: 获取项目成员列表
*/
public PageResponse<ProjectMemberDTO> getMembers(UUID projectId, Pageable pageable) {
// 验证项目存在
if (!projectRepository.existsById(projectId)) {
throw new BusinessException(ErrorCode.PROJECT_002);
}
// 分页查询用户项目关联
Page<UserProject> userProjectPage = userProjectRepository.findByProjectId(projectId, pageable);
// 获取用户ID列表
List<UUID> userIds = userProjectPage.getContent().stream()
.map(UserProject::getUserId)
.collect(Collectors.toList());
// 批量查询用户信息
Map<UUID, User> userMap;
if (!userIds.isEmpty()) {
List<User> users = userRepository.findAllById(userIds);
userMap = users.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
} else {
userMap = Collections.emptyMap();
}
// 使用 final 变量在 lambda
final Map<UUID, User> finalUserMap = userMap;
// 构建DTO
List<ProjectMemberDTO> members = userProjectPage.getContent().stream()
.map(up -> {
User user = finalUserMap.get(up.getUserId());
ProjectMemberDTO dto = new ProjectMemberDTO();
dto.setId(up.getId());
dto.setUserId(up.getUserId());
dto.setRoleInProject(up.getRoleInProject());
dto.setJoinedAt(up.getJoinedAt());
if (user != null) {
dto.setUsername(user.getUsername());
dto.setRealName(user.getRealName());
dto.setPhone(user.getPhone());
dto.setAvatar(user.getAvatar());
}
return dto;
})
.collect(Collectors.toList());
return PageResponse.of(
members,
userProjectPage.getNumber(),
userProjectPage.getSize(),
userProjectPage.getTotalElements()
);
}
/**
* PM-003: 添加项目成员
*/
@Transactional
public void addMembers(UUID projectId, AddMemberRequest request) {
// 验证项目存在
if (!projectRepository.existsById(projectId)) {
throw new BusinessException(ErrorCode.PROJECT_002);
}
List<UserProject> userProjects = new ArrayList<>();
for (UUID userId : request.getUserIds()) {
// 验证用户存在
if (!userRepository.existsById(userId)) {
throw new BusinessException(ErrorCode.USER_003);
}
// 检查是否已是成员
if (userProjectRepository.existsByUserIdAndProjectId(userId, projectId)) {
throw new BusinessException(ErrorCode.PROJECT_004);
}
UserProject userProject = new UserProject();
userProject.setUserId(userId);
userProject.setProjectId(projectId);
userProject.setRoleInProject(request.getRoleInProject());
userProject.setJoinedAt(LocalDateTime.now());
userProjects.add(userProject);
}
userProjectRepository.saveAll(userProjects);
}
/**
* PM-003: 移除项目成员
*/
@Transactional
public void removeMember(UUID projectId, UUID memberId) {
// 验证成员关联存在
if (!userProjectRepository.existsById(memberId)) {
throw new BusinessException(ErrorCode.PROJECT_005);
}
userProjectRepository.deleteById(memberId);
}
}

View File

@ -1,36 +1,112 @@
package com.ether.pms.mdm.service; package com.ether.pms.mdm.service;
import com.ether.pms.mdm.dto.PageResponse;
import com.ether.pms.mdm.dto.ProjectQueryRequest;
import com.ether.pms.mdm.dto.ProjectSelectorItem;
import com.ether.pms.mdm.entity.Project; import com.ether.pms.mdm.entity.Project;
import com.ether.pms.mdm.repository.ProjectRepository; import com.ether.pms.mdm.repository.ProjectRepository;
import com.ether.pms.common.BusinessException; import com.ether.pms.common.BusinessException;
import com.ether.pms.common.ErrorCode; import com.ether.pms.common.ErrorCode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.Year;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class ProjectService { public class ProjectService {
private final ProjectRepository projectRepository; private final ProjectRepository projectRepository;
public List<Project> findAll() { public List<Project> findAll() {
return projectRepository.findAll(); return projectRepository.findAll();
} }
public Project findById(UUID id) { public Project findById(UUID id) {
return projectRepository.findById(id) return projectRepository.findById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_002)); .orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_002));
} }
public Project findByCode(String code) { public Project findByCode(String code) {
return projectRepository.findByCode(code) return projectRepository.findByCode(code)
.orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_002)); .orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_002));
} }
/**
* PM-001: 分页查询项目
*/
public PageResponse<Project> queryProjects(ProjectQueryRequest request) {
Sort sort = Sort.by(
"DESC".equalsIgnoreCase(request.getSortDirection())
? Sort.Direction.DESC
: Sort.Direction.ASC,
request.getSortBy()
);
Pageable pageable = PageRequest.of(request.getPage(), request.getSize(), sort);
Page<Project> projectPage = projectRepository.searchProjects(
request.getKeyword(),
request.getStatus(),
pageable
);
return PageResponse.of(
projectPage.getContent(),
projectPage.getNumber(),
projectPage.getSize(),
projectPage.getTotalElements()
);
}
/**
* PM-010: 获取项目选择器列表
*/
public List<ProjectSelectorItem> getSelectorList() {
List<Project> projects = projectRepository.findActiveProjectsForSelector();
return projects.stream()
.map(p -> new ProjectSelectorItem(
p.getId().toString(),
p.getCode(),
p.getName(),
p.getStatus()
))
.collect(Collectors.toList());
}
/**
* PM-005: 生成项目编码
* 格式: PRJ-YYYYNNNN (年份+4位序号)
*/
public String generateCode() {
int year = Year.now().getValue();
int sequence = 1;
// 尝试生成唯一编码
String code;
do {
code = String.format("PRJ-%d%04d", year, sequence);
sequence++;
// 防止无限循环
if (sequence > 9999) {
// 使用随机数作为后备方案
code = String.format("PRJ-%d%04d", year,
ThreadLocalRandom.current().nextInt(1, 10000));
break;
}
} while (projectRepository.existsByCode(code));
return code;
}
@Transactional @Transactional
public Project create(Project project) { public Project create(Project project) {
if (projectRepository.existsByCode(project.getCode())) { if (projectRepository.existsByCode(project.getCode())) {
@ -38,11 +114,19 @@ public class ProjectService {
} }
return projectRepository.save(project); return projectRepository.save(project);
} }
@Transactional @Transactional
public Project update(UUID id, Project project) { public Project update(UUID id, Project project) {
Project existing = findById(id); Project existing = findById(id);
// 检查编码是否被其他项目使用
if (project.getCode() != null && !project.getCode().equals(existing.getCode())) {
if (projectRepository.existsByCodeAndIdNot(project.getCode(), id)) {
throw new BusinessException(ErrorCode.PROJECT_001);
}
existing.setCode(project.getCode());
}
if (project.getName() != null) { if (project.getName() != null) {
existing.setName(project.getName()); existing.setName(project.getName());
} }
@ -94,12 +178,58 @@ public class ProjectService {
if (project.getContactPhone() != null) { if (project.getContactPhone() != null) {
existing.setContactPhone(project.getContactPhone()); existing.setContactPhone(project.getContactPhone());
} }
return projectRepository.save(existing); return projectRepository.save(existing);
} }
@Transactional @Transactional
public void delete(UUID id) { public void delete(UUID id) {
projectRepository.deleteById(id); projectRepository.deleteById(id);
} }
/**
* PM-006: 变更项目状态
* 状态转换规则:
* - ACTIVE -> INACTIVE (停用)
* - INACTIVE -> ACTIVE (启用)
* - ACTIVE -> ARCHIVED (归档)
* - INACTIVE -> ARCHIVED (归档)
*/
@Transactional
public void changeStatus(UUID projectId, String newStatus, String reason) {
Project project = findById(projectId);
String currentStatus = project.getStatus();
// 验证状态转换是否合法
if (!isValidStatusTransition(currentStatus, newStatus)) {
throw new BusinessException(ErrorCode.PROJECT_003);
}
project.setStatus(newStatus);
projectRepository.save(project);
// TODO: 保存状态变更历史记录如需要
}
/**
* 验证状态转换是否合法
*/
private boolean isValidStatusTransition(String currentStatus, String newStatus) {
if (currentStatus == null || newStatus == null) {
return false;
}
// 相同状态不需要转换
if (currentStatus.equals(newStatus)) {
return true;
}
// 定义允许的状态转换
return switch (currentStatus) {
case "ACTIVE" -> "INACTIVE".equals(newStatus) || "ARCHIVED".equals(newStatus);
case "INACTIVE" -> "ACTIVE".equals(newStatus) || "ARCHIVED".equals(newStatus);
case "ARCHIVED" -> false; // 归档状态不能转换到其他状态
default -> false;
};
}
} }

View File

@ -0,0 +1,113 @@
package com.ether.pms.mdm.service;
import com.ether.pms.mdm.entity.ProjectStatistics;
import com.ether.pms.mdm.repository.ProjectStatisticsRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
/**
* 项目统计服务
*/
@Service
@RequiredArgsConstructor
public class ProjectStatisticsService {
private final ProjectStatisticsRepository statisticsRepository;
/**
* 获取项目统计信息
*/
public ProjectStatistics getStatistics(UUID projectId) {
return statisticsRepository.findByProjectId(projectId)
.orElseGet(() -> {
ProjectStatistics stats = new ProjectStatistics();
stats.setProjectId(projectId);
return statisticsRepository.save(stats);
});
}
/**
* 同步成员统计数量
*/
@Transactional
public void syncMemberStatistics(UUID projectId, int count) {
ProjectStatistics stats = getStatistics(projectId);
stats.setMemberCount(count);
stats.setLastSyncedAt(LocalDateTime.now());
statisticsRepository.save(stats);
}
/**
* 增加成员数量
*/
@Transactional
public void incrementMemberCount(UUID projectId) {
ProjectStatistics stats = getStatistics(projectId);
stats.setMemberCount(stats.getMemberCount() + 1);
stats.setLastSyncedAt(LocalDateTime.now());
statisticsRepository.save(stats);
}
/**
* 减少成员数量
*/
@Transactional
public void decrementMemberCount(UUID projectId) {
ProjectStatistics stats = getStatistics(projectId);
int newCount = Math.max(0, stats.getMemberCount() - 1);
stats.setMemberCount(newCount);
stats.setLastSyncedAt(LocalDateTime.now());
statisticsRepository.save(stats);
}
/**
* 更新建筑数量
*/
@Transactional
public void updateBuildingCount(UUID projectId, int count) {
ProjectStatistics stats = getStatistics(projectId);
stats.setBuildingCount(count);
stats.setLastSyncedAt(LocalDateTime.now());
statisticsRepository.save(stats);
}
/**
* 更新单元数量
*/
@Transactional
public void updateUnitCount(UUID projectId, int count) {
ProjectStatistics stats = getStatistics(projectId);
stats.setUnitCount(count);
stats.setLastSyncedAt(LocalDateTime.now());
statisticsRepository.save(stats);
}
/**
* 更新房间数量
*/
@Transactional
public void updateRoomCount(UUID projectId, int count) {
ProjectStatistics stats = getStatistics(projectId);
stats.setRoomCount(count);
stats.setLastSyncedAt(LocalDateTime.now());
statisticsRepository.save(stats);
}
/**
* 初始化项目统计
*/
@Transactional
public ProjectStatistics initializeStatistics(UUID projectId) {
if (statisticsRepository.existsByProjectId(projectId)) {
return getStatistics(projectId);
}
ProjectStatistics stats = new ProjectStatistics();
stats.setProjectId(projectId);
return statisticsRepository.save(stats);
}
}

View File

@ -1,91 +1,225 @@
package com.ether.pms.mdm.service; package com.ether.pms.mdm.service;
import com.ether.pms.mdm.entity.SpaceNode;
import com.ether.pms.mdm.repository.SpaceNodeRepository;
import com.ether.pms.common.BusinessException; import com.ether.pms.common.BusinessException;
import com.ether.pms.common.ErrorCode; import com.ether.pms.common.ErrorCode;
import com.ether.pms.mdm.dto.SpaceNodeCreateDTO;
import com.ether.pms.mdm.dto.SpaceNodeTreeDTO;
import com.ether.pms.mdm.dto.SpaceNodeUpdateDTO;
import com.ether.pms.mdm.entity.SpaceNode;
import com.ether.pms.mdm.repository.SpaceNodeRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SpaceNodeService { public class SpaceNodeService {
private final SpaceNodeRepository spaceNodeRepository; private final SpaceNodeRepository spaceNodeRepository;
public List<SpaceNode> findAll() { public List<SpaceNode> findAll() {
return spaceNodeRepository.findAll(); return spaceNodeRepository.findByIsDeletedFalse();
} }
public SpaceNode findById(UUID id) { public SpaceNode findById(UUID id) {
return spaceNodeRepository.findById(id) return spaceNodeRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow(() -> new BusinessException(ErrorCode.SPACE_002)); .orElseThrow(() -> new BusinessException(ErrorCode.SPACE_002));
} }
public List<SpaceNode> findByProject(String projectCode) { public List<SpaceNode> findByProjectId(UUID projectId) {
return spaceNodeRepository.findByProjectCode(projectCode); return spaceNodeRepository.findByProjectIdAndIsDeletedFalseOrderBySortOrderAsc(projectId);
} }
public List<SpaceNode> findByProjectAndType(String projectCode, String nodeType) { public List<SpaceNode> findByProjectIdAndNodeType(UUID projectId, SpaceNode.NodeType nodeType) {
return spaceNodeRepository.findByProjectCodeAndNodeType(projectCode, nodeType); return spaceNodeRepository.findByProjectIdAndNodeTypeAndIsDeletedFalse(projectId, nodeType);
} }
public List<SpaceNode> findByParent(String parentCode) { public List<SpaceNode> findRootsByProjectId(UUID projectId) {
return spaceNodeRepository.findByParentCode(parentCode); return spaceNodeRepository.findByProjectIdAndParentIdIsNullAndIsDeletedFalse(projectId);
} }
public List<SpaceNode> findChildren(UUID parentId) {
return spaceNodeRepository.findByParentIdAndIsDeletedFalseOrderBySortOrderAsc(parentId);
}
public List<SpaceNode> findByParentCode(String parentCode) {
return spaceNodeRepository.findByParentCodeAndIsDeletedFalse(parentCode);
}
public List<SpaceNodeTreeDTO> getTreeByProjectId(UUID projectId) {
List<SpaceNode> allNodes = findByProjectId(projectId);
return buildTree(allNodes);
}
public List<SpaceNodeTreeDTO> buildTree(List<SpaceNode> nodes) {
if (nodes == null || nodes.isEmpty()) {
return new ArrayList<>();
}
List<SpaceNodeTreeDTO> rootNodes = new ArrayList<>();
List<SpaceNodeTreeDTO> allDtos = nodes.stream()
.map(SpaceNodeTreeDTO::fromEntity)
.collect(Collectors.toList());
for (SpaceNodeTreeDTO dto : allDtos) {
if (dto.getParentId() == null) {
rootNodes.add(dto);
} else {
for (SpaceNodeTreeDTO parent : allDtos) {
if (dto.getParentId().equals(parent.getId())) {
parent.addChild(dto);
break;
}
}
}
}
return rootNodes;
}
@Transactional @Transactional
public SpaceNode create(SpaceNode spaceNode) { public SpaceNode create(SpaceNodeCreateDTO dto) {
if (spaceNodeRepository.existsByCode(spaceNode.getCode())) { if (spaceNodeRepository.existsByProjectIdAndCode(dto.getProjectId(), dto.getCode())) {
throw new BusinessException(ErrorCode.SPACE_001); throw new BusinessException(ErrorCode.SPACE_001);
} }
return spaceNodeRepository.save(spaceNode);
SpaceNode node = new SpaceNode();
node.setProjectId(dto.getProjectId());
node.setCode(dto.getCode());
node.setName(dto.getName());
node.setFullName(dto.getFullName());
node.setShortName(dto.getShortName());
node.setNodeCategory(dto.getNodeCategory());
node.setNodeType(dto.getNodeType());
node.setUsageType(dto.getUsageType());
node.setSortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0);
node.setStatus(dto.getStatus() != null ? dto.getStatus() : "ACTIVE");
node.setDeliveryStatus(dto.getDeliveryStatus());
node.setDecorationStatus(dto.getDecorationStatus());
node.setBuildingArea(dto.getBuildingArea());
node.setUsableArea(dto.getUsableArea());
node.setSharedArea(dto.getSharedArea());
node.setLandArea(dto.getLandArea());
node.setLongitude(dto.getLongitude());
node.setLatitude(dto.getLatitude());
node.setAltitude(dto.getAltitude());
node.setFloorNumber(dto.getFloorNumber());
node.setProvince(dto.getProvince());
node.setCity(dto.getCity());
node.setDistrict(dto.getDistrict());
node.setStreet(dto.getStreet());
node.setAddress(dto.getAddress());
node.setAttributes(dto.getAttributes());
if (dto.getParentId() != null) {
SpaceNode parent = findById(dto.getParentId());
node.setParentId(parent.getId());
node.setParentCode(parent.getCode());
node.setLevel(parent.getLevel() + 1);
node.setTreePath(parent.getTreePath() + "." + node.getId());
node.setTreePathName(parent.getTreePathName() + "/" + node.getName());
} else {
node.setLevel(0);
node.setTreePath(node.getId().toString());
node.setTreePathName(node.getName());
}
return spaceNodeRepository.save(node);
} }
@Transactional @Transactional
public SpaceNode update(UUID id, SpaceNode spaceNode) { public SpaceNode update(UUID id, SpaceNodeUpdateDTO dto) {
SpaceNode existing = findById(id); SpaceNode existing = findById(id);
if (spaceNode.getName() != null) { if (dto.getName() != null) {
existing.setName(spaceNode.getName()); existing.setName(dto.getName());
} }
if (spaceNode.getNodeType() != null) { if (dto.getFullName() != null) {
existing.setNodeType(spaceNode.getNodeType()); existing.setFullName(dto.getFullName());
} }
if (spaceNode.getParentCode() != null) { if (dto.getShortName() != null) {
existing.setParentCode(spaceNode.getParentCode()); existing.setShortName(dto.getShortName());
} }
if (spaceNode.getSortOrder() != null) { if (dto.getNodeCategory() != null) {
existing.setSortOrder(spaceNode.getSortOrder()); existing.setNodeCategory(dto.getNodeCategory());
} }
if (spaceNode.getBuilding() != null) { if (dto.getNodeType() != null) {
existing.setBuilding(spaceNode.getBuilding()); existing.setNodeType(dto.getNodeType());
} }
if (spaceNode.getUnit() != null) { if (dto.getUsageType() != null) {
existing.setUnit(spaceNode.getUnit()); existing.setUsageType(dto.getUsageType());
} }
if (spaceNode.getFloor() != null) { if (dto.getSortOrder() != null) {
existing.setFloor(spaceNode.getFloor()); existing.setSortOrder(dto.getSortOrder());
} }
if (spaceNode.getRoomNumber() != null) { if (dto.getStatus() != null) {
existing.setRoomNumber(spaceNode.getRoomNumber()); existing.setStatus(dto.getStatus());
} }
if (spaceNode.getArea() != null) { if (dto.getDeliveryStatus() != null) {
existing.setArea(spaceNode.getArea()); existing.setDeliveryStatus(dto.getDeliveryStatus());
} }
if (spaceNode.getStatus() != null) { if (dto.getDecorationStatus() != null) {
existing.setStatus(spaceNode.getStatus()); existing.setDecorationStatus(dto.getDecorationStatus());
} }
if (dto.getBuildingArea() != null) {
existing.setBuildingArea(dto.getBuildingArea());
}
if (dto.getUsableArea() != null) {
existing.setUsableArea(dto.getUsableArea());
}
if (dto.getSharedArea() != null) {
existing.setSharedArea(dto.getSharedArea());
}
if (dto.getLandArea() != null) {
existing.setLandArea(dto.getLandArea());
}
if (dto.getLongitude() != null) {
existing.setLongitude(dto.getLongitude());
}
if (dto.getLatitude() != null) {
existing.setLatitude(dto.getLatitude());
}
if (dto.getAltitude() != null) {
existing.setAltitude(dto.getAltitude());
}
if (dto.getFloorNumber() != null) {
existing.setFloorNumber(dto.getFloorNumber());
}
if (dto.getProvince() != null) {
existing.setProvince(dto.getProvince());
}
if (dto.getCity() != null) {
existing.setCity(dto.getCity());
}
if (dto.getDistrict() != null) {
existing.setDistrict(dto.getDistrict());
}
if (dto.getStreet() != null) {
existing.setStreet(dto.getStreet());
}
if (dto.getAddress() != null) {
existing.setAddress(dto.getAddress());
}
if (dto.getAttributes() != null) {
existing.setAttributes(dto.getAttributes());
}
return spaceNodeRepository.save(existing); return spaceNodeRepository.save(existing);
} }
@Transactional @Transactional
public void delete(UUID id) { public void delete(UUID id) {
spaceNodeRepository.deleteById(id); SpaceNode node = findById(id);
List<SpaceNode> children = findChildren(id);
if (!children.isEmpty()) {
throw new BusinessException(ErrorCode.SPACE_003);
}
node.setIsDeleted(true);
spaceNodeRepository.save(node);
} }
} }

View File

@ -0,0 +1,157 @@
package com.ether.pms.mdm.entity;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class SpaceNodeTest {
@Test
void testSpaceNodeCreation() {
SpaceNode node = new SpaceNode();
node.setId(UUID.randomUUID());
node.setCode("B001");
node.setName("1号楼");
node.setNodeType(SpaceNode.NodeType.BUILDING);
node.setNodeCategory(SpaceNode.NodeCategory.BUILDING);
node.setProjectId(UUID.randomUUID());
node.setParentId(UUID.randomUUID());
node.setLevel(1);
node.setSortOrder(1);
node.setStatus("ACTIVE");
node.setBuildingArea(new BigDecimal("12000.00"));
node.setUsableArea(new BigDecimal("10000.00"));
node.setLongitude(new BigDecimal("121.473701"));
node.setLatitude(new BigDecimal("31.230416"));
assertNotNull(node.getId());
assertEquals("B001", node.getCode());
assertEquals("1号楼", node.getName());
assertEquals(SpaceNode.NodeType.BUILDING, node.getNodeType());
assertEquals(SpaceNode.NodeCategory.BUILDING, node.getNodeCategory());
assertEquals(1, node.getLevel());
assertEquals("ACTIVE", node.getStatus());
}
@Test
void testSpaceNodePrePersist() {
SpaceNode node = new SpaceNode();
node.setCode("B001");
node.setName("1号楼");
node.setNodeType(SpaceNode.NodeType.BUILDING);
node.setNodeCategory(SpaceNode.NodeCategory.BUILDING);
node.setProjectId(UUID.randomUUID());
node.prePersist();
assertNotNull(node.getCreatedAt());
assertNotNull(node.getUpdatedAt());
assertEquals("ACTIVE", node.getStatus());
assertEquals(0, node.getSortOrder());
assertEquals(0, node.getLevel());
assertFalse(node.getIsDeleted());
}
@Test
void testSpaceNodeTreePathGeneration() {
SpaceNode parent = new SpaceNode();
parent.setId(UUID.randomUUID());
parent.setCode("B001");
parent.setTreePath("B001");
SpaceNode child = new SpaceNode();
child.setId(UUID.randomUUID());
child.setCode("B001-U1");
child.setParentId(parent.getId());
child.setParentCode("B001");
child.setTreePath(parent.getTreePath() + "." + child.getId());
assertEquals("B001", parent.getTreePath());
assertTrue(child.getTreePath().startsWith("B001."));
}
@Test
void testSpaceNodeTypes() {
assertNotNull(SpaceNode.NodeType.BUILDING);
assertEquals("楼栋", SpaceNode.NodeType.BUILDING.getDesc());
assertEquals(SpaceNode.NodeCategory.BUILDING, SpaceNode.NodeType.BUILDING.getCategory());
assertNotNull(SpaceNode.NodeType.UNIT);
assertEquals("单元", SpaceNode.NodeType.UNIT.getDesc());
assertNotNull(SpaceNode.NodeType.ROOM);
assertEquals("房间", SpaceNode.NodeType.ROOM.getDesc());
assertNotNull(SpaceNode.NodeType.PARKING_SPACE);
assertEquals("车位", SpaceNode.NodeType.PARKING_SPACE.getDesc());
assertEquals(SpaceNode.NodeCategory.PARKING, SpaceNode.NodeType.PARKING_SPACE.getCategory());
}
@Test
void testSpaceNodeCategory() {
assertNotNull(SpaceNode.NodeCategory.BUILDING);
assertEquals("建筑空间", SpaceNode.NodeCategory.BUILDING.getDesc());
assertNotNull(SpaceNode.NodeCategory.PARKING);
assertEquals("停车空间", SpaceNode.NodeCategory.PARKING.getDesc());
assertNotNull(SpaceNode.NodeCategory.FACILITY);
assertEquals("设施空间", SpaceNode.NodeCategory.FACILITY.getDesc());
assertNotNull(SpaceNode.NodeCategory.AREA);
assertEquals("区域空间", SpaceNode.NodeCategory.AREA.getDesc());
}
@Test
void testSpaceNodeFloorNumber() {
SpaceNode floor1 = new SpaceNode();
floor1.setFloorNumber(5);
assertEquals(5, floor1.getFloorNumber());
SpaceNode basement = new SpaceNode();
basement.setFloorNumber(-1);
assertEquals(-1, basement.getFloorNumber());
}
@Test
void testSpaceNodeStatus() {
SpaceNode node = new SpaceNode();
node.setDeliveryStatus("DELIVERED");
assertEquals("DELIVERED", node.getDeliveryStatus());
node.setDecorationStatus("FINE");
assertEquals("FINE", node.getDecorationStatus());
}
@Test
void testSpaceNodeAreaFields() {
SpaceNode node = new SpaceNode();
node.setBuildingArea(new BigDecimal("1500.50"));
node.setUsableArea(new BigDecimal("1200.00"));
node.setSharedArea(new BigDecimal("300.50"));
node.setLandArea(new BigDecimal("5000.00"));
assertEquals(new BigDecimal("1500.50"), node.getBuildingArea());
assertEquals(new BigDecimal("1200.00"), node.getUsableArea());
assertEquals(new BigDecimal("300.50"), node.getSharedArea());
assertEquals(new BigDecimal("5000.00"), node.getLandArea());
}
@Test
void testSpaceNodeAddressFields() {
SpaceNode node = new SpaceNode();
node.setProvince("上海市");
node.setCity("上海市");
node.setDistrict("徐汇区");
node.setStreet("漕河泾路");
node.setAddress("漕河泾路188号");
assertEquals("上海市", node.getProvince());
assertEquals("上海市", node.getCity());
assertEquals("徐汇区", node.getDistrict());
assertEquals("漕河泾路", node.getStreet());
assertEquals("漕河泾路188号", node.getAddress());
}
}

View File

@ -0,0 +1,203 @@
package com.ether.pms.mdm.service;
import com.ether.pms.auth.entity.User;
import com.ether.pms.auth.entity.UserProject;
import com.ether.pms.auth.repository.UserProjectRepository;
import com.ether.pms.auth.repository.UserRepository;
import com.ether.pms.mdm.dto.AddMemberRequest;
import com.ether.pms.mdm.dto.PageResponse;
import com.ether.pms.mdm.dto.ProjectMemberDTO;
import com.ether.pms.mdm.entity.Project;
import com.ether.pms.mdm.repository.ProjectRepository;
import com.ether.pms.common.BusinessException;
import com.ether.pms.common.ErrorCode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import java.time.LocalDateTime;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* ProjectMemberService 测试类 - TDD方式开发
*/
@ExtendWith(MockitoExtension.class)
class ProjectMemberServiceTest {
@Mock
private UserProjectRepository userProjectRepository;
@Mock
private UserRepository userRepository;
@Mock
private ProjectRepository projectRepository;
@InjectMocks
private ProjectMemberService projectMemberService;
private UUID testProjectId;
private UUID testUserId;
private Project testProject;
private User testUser;
private UserProject testUserProject;
@BeforeEach
void setUp() {
testProjectId = UUID.randomUUID();
testUserId = UUID.randomUUID();
testProject = new Project();
testProject.setId(testProjectId);
testProject.setCode("PRJ-001");
testProject.setName("测试项目");
testProject.setStatus("ACTIVE");
testUser = new User();
testUser.setId(testUserId);
testUser.setUsername("testuser");
testUser.setRealName("测试用户");
testUser.setPhone("13800138000");
testUser.setAvatar("avatar.png");
testUserProject = new UserProject();
testUserProject.setId(UUID.randomUUID());
testUserProject.setUserId(testUserId);
testUserProject.setProjectId(testProjectId);
testUserProject.setRoleInProject("member");
testUserProject.setJoinedAt(LocalDateTime.now());
}
// ========================================
// PM-003: 成员管理测试
// ========================================
@Test
void testGetMembers_shouldReturnPagedMembers() {
// Given
List<UserProject> userProjects = new ArrayList<>();
userProjects.add(testUserProject);
Page<UserProject> userProjectPage = new PageImpl<>(userProjects, PageRequest.of(0, 20), 1);
when(projectRepository.existsById(testProjectId)).thenReturn(true);
when(userProjectRepository.findByProjectId(eq(testProjectId), any(Pageable.class)))
.thenReturn(userProjectPage);
when(userRepository.findAllById(any())).thenReturn(List.of(testUser));
// When
PageResponse<ProjectMemberDTO> result = projectMemberService.getMembers(testProjectId, PageRequest.of(0, 20));
// Then
assertNotNull(result);
assertEquals(1, result.getTotalElements());
assertEquals(1, result.getContent().size());
assertEquals("testuser", result.getContent().get(0).getUsername());
assertEquals("member", result.getContent().get(0).getRoleInProject());
}
@Test
void testGetMembers_projectNotFound_shouldThrowException() {
// Given
when(projectRepository.existsById(testProjectId)).thenReturn(false);
// When & Then
BusinessException exception = assertThrows(BusinessException.class, () -> {
projectMemberService.getMembers(testProjectId, PageRequest.of(0, 20));
});
assertEquals(ErrorCode.PROJECT_002.getCode(), exception.getCode());
}
@Test
void testAddMembers_shouldAddMembersSuccessfully() {
// Given
AddMemberRequest request = new AddMemberRequest();
request.setUserIds(List.of(testUserId));
request.setRoleInProject("member");
when(projectRepository.existsById(testProjectId)).thenReturn(true);
when(userRepository.existsById(testUserId)).thenReturn(true);
when(userProjectRepository.existsByUserIdAndProjectId(testUserId, testProjectId)).thenReturn(false);
when(userProjectRepository.saveAll(any())).thenReturn(List.of(testUserProject));
// When
projectMemberService.addMembers(testProjectId, request);
// Then
verify(userProjectRepository).saveAll(any());
}
@Test
void testAddMembers_memberAlreadyExists_shouldThrowException() {
// Given
AddMemberRequest request = new AddMemberRequest();
request.setUserIds(List.of(testUserId));
request.setRoleInProject("member");
when(projectRepository.existsById(testProjectId)).thenReturn(true);
when(userRepository.existsById(testUserId)).thenReturn(true);
when(userProjectRepository.existsByUserIdAndProjectId(testUserId, testProjectId)).thenReturn(true);
// When & Then
BusinessException exception = assertThrows(BusinessException.class, () -> {
projectMemberService.addMembers(testProjectId, request);
});
assertEquals(ErrorCode.PROJECT_004.getCode(), exception.getCode());
}
@Test
void testAddMembers_userNotFound_shouldThrowException() {
// Given
AddMemberRequest request = new AddMemberRequest();
request.setUserIds(List.of(testUserId));
request.setRoleInProject("member");
when(projectRepository.existsById(testProjectId)).thenReturn(true);
when(userRepository.existsById(testUserId)).thenReturn(false);
// When & Then
BusinessException exception = assertThrows(BusinessException.class, () -> {
projectMemberService.addMembers(testProjectId, request);
});
assertEquals(ErrorCode.USER_003.getCode(), exception.getCode());
}
@Test
void testRemoveMember_shouldRemoveMemberSuccessfully() {
// Given
UUID memberRelationId = UUID.randomUUID();
when(userProjectRepository.existsById(memberRelationId)).thenReturn(true);
doNothing().when(userProjectRepository).deleteById(memberRelationId);
// When
projectMemberService.removeMember(testProjectId, memberRelationId);
// Then
verify(userProjectRepository).deleteById(memberRelationId);
}
@Test
void testRemoveMember_memberNotFound_shouldThrowException() {
// Given
UUID memberRelationId = UUID.randomUUID();
when(userProjectRepository.existsById(memberRelationId)).thenReturn(false);
// When & Then
BusinessException exception = assertThrows(BusinessException.class, () -> {
projectMemberService.removeMember(testProjectId, memberRelationId);
});
assertEquals(ErrorCode.PROJECT_005.getCode(), exception.getCode());
}
}

View File

@ -0,0 +1,216 @@
package com.ether.pms.mdm.service;
import com.ether.pms.mdm.dto.PageResponse;
import com.ether.pms.mdm.dto.ProjectQueryRequest;
import com.ether.pms.mdm.entity.Project;
import com.ether.pms.mdm.repository.ProjectRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* ProjectService 测试类 - TDD方式开发
*/
@ExtendWith(MockitoExtension.class)
class ProjectServiceTest {
@Mock
private ProjectRepository projectRepository;
@InjectMocks
private ProjectService projectService;
private Project testProject;
private UUID testProjectId;
@BeforeEach
void setUp() {
testProjectId = UUID.randomUUID();
testProject = new Project();
testProject.setId(testProjectId);
testProject.setCode("PRJ-001");
testProject.setName("测试项目");
testProject.setStatus("ACTIVE");
}
// ========================================
// PM-001: 分页查询测试
// ========================================
@Test
void testQueryProjects_withKeyword_shouldReturnFilteredResults() {
// Given
ProjectQueryRequest request = new ProjectQueryRequest();
request.setKeyword("测试");
request.setPage(0);
request.setSize(20);
List<Project> projects = new ArrayList<>();
projects.add(testProject);
Page<Project> projectPage = new PageImpl<>(projects, PageRequest.of(0, 20), 1);
when(projectRepository.searchProjects(eq("测试"), eq(null), any(Pageable.class)))
.thenReturn(projectPage);
// When
PageResponse<Project> result = projectService.queryProjects(request);
// Then
assertNotNull(result);
assertEquals(1, result.getTotalElements());
assertEquals(1, result.getContent().size());
assertEquals("测试项目", result.getContent().get(0).getName());
verify(projectRepository).searchProjects(eq("测试"), eq(null), any(Pageable.class));
}
@Test
void testQueryProjects_withStatusFilter_shouldReturnFilteredResults() {
// Given
ProjectQueryRequest request = new ProjectQueryRequest();
request.setStatus("ACTIVE");
request.setPage(0);
request.setSize(20);
List<Project> projects = new ArrayList<>();
projects.add(testProject);
Page<Project> projectPage = new PageImpl<>(projects, PageRequest.of(0, 20), 1);
when(projectRepository.searchProjects(eq(null), eq("ACTIVE"), any(Pageable.class)))
.thenReturn(projectPage);
// When
PageResponse<Project> result = projectService.queryProjects(request);
// Then
assertNotNull(result);
assertEquals(1, result.getTotalElements());
assertEquals("ACTIVE", result.getContent().get(0).getStatus());
verify(projectRepository).searchProjects(eq(null), eq("ACTIVE"), any(Pageable.class));
}
@Test
void testQueryProjects_withPagination_shouldReturnPagedResults() {
// Given
ProjectQueryRequest request = new ProjectQueryRequest();
request.setPage(1);
request.setSize(10);
request.setSortBy("name");
request.setSortDirection("ASC");
List<Project> projects = new ArrayList<>();
Page<Project> projectPage = new PageImpl<>(projects, PageRequest.of(1, 10), 25);
when(projectRepository.searchProjects(eq(null), eq(null), any(Pageable.class)))
.thenReturn(projectPage);
// When
PageResponse<Project> result = projectService.queryProjects(request);
// Then
assertNotNull(result);
assertEquals(1, result.getPage());
assertEquals(10, result.getSize());
assertEquals(25, result.getTotalElements());
assertEquals(3, result.getTotalPages());
assertFalse(result.isFirst());
assertFalse(result.isLast());
}
@Test
void testQueryProjects_emptyResult_shouldReturnEmptyPage() {
// Given
ProjectQueryRequest request = new ProjectQueryRequest();
request.setKeyword("不存在");
Page<Project> emptyPage = new PageImpl<>(new ArrayList<>(), PageRequest.of(0, 20), 0);
when(projectRepository.searchProjects(eq("不存在"), eq(null), any(Pageable.class)))
.thenReturn(emptyPage);
// When
PageResponse<Project> result = projectService.queryProjects(request);
// Then
assertNotNull(result);
assertTrue(result.getContent().isEmpty());
assertEquals(0, result.getTotalElements());
}
// ========================================
// PM-010: 选择器列表测试
// ========================================
@Test
void testGetSelectorList_shouldReturnActiveProjects() {
// Given
List<Project> projects = new ArrayList<>();
projects.add(testProject);
Project project2 = new Project();
project2.setId(UUID.randomUUID());
project2.setCode("PRJ-002");
project2.setName("测试项目2");
project2.setStatus("INACTIVE");
projects.add(project2);
when(projectRepository.findActiveProjectsForSelector()).thenReturn(projects);
// When
var result = projectService.getSelectorList();
// Then
assertNotNull(result);
assertEquals(2, result.size());
verify(projectRepository).findActiveProjectsForSelector();
}
// ========================================
// PM-005: 编码生成测试
// ========================================
@Test
void testGenerateCode_shouldReturnUniqueCode() {
// Given
when(projectRepository.existsByCode(anyString())).thenReturn(false);
// When
String code = projectService.generateCode();
// Then
assertNotNull(code);
assertTrue(code.startsWith("PRJ-"));
verify(projectRepository).existsByCode(code);
}
@Test
void testGenerateCode_shouldRetryIfCodeExists() {
// Given - 第一个编码存在第二个编码不存在
when(projectRepository.existsByCode("PRJ-20260001")).thenReturn(true);
when(projectRepository.existsByCode("PRJ-20260002")).thenReturn(false);
// When
String code = projectService.generateCode();
// Then
assertNotNull(code);
assertTrue(code.startsWith("PRJ-"));
// 验证最终返回的编码不存在
verify(projectRepository).existsByCode(code);
}
}

View File

@ -0,0 +1,140 @@
package com.ether.pms.mdm.service;
import com.ether.pms.mdm.entity.ProjectStatistics;
import com.ether.pms.mdm.repository.ProjectStatisticsRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
* ProjectStatisticsService 测试类
*/
@ExtendWith(MockitoExtension.class)
class ProjectStatisticsServiceTest {
@Mock
private ProjectStatisticsRepository statisticsRepository;
@InjectMocks
private ProjectStatisticsService statisticsService;
private UUID projectId;
private ProjectStatistics testStatistics;
@BeforeEach
void setUp() {
projectId = UUID.randomUUID();
testStatistics = new ProjectStatistics();
testStatistics.setId(UUID.randomUUID());
testStatistics.setProjectId(projectId);
testStatistics.setMemberCount(10);
testStatistics.setBuildingCount(5);
testStatistics.setUnitCount(20);
testStatistics.setRoomCount(100);
}
@Test
void testGetStatistics_shouldReturnStats_whenExists() {
// Given
when(statisticsRepository.findByProjectId(projectId)).thenReturn(Optional.of(testStatistics));
// When
ProjectStatistics result = statisticsService.getStatistics(projectId);
// Then
assertNotNull(result);
assertEquals(10, result.getMemberCount());
assertEquals(5, result.getBuildingCount());
verify(statisticsRepository).findByProjectId(projectId);
}
@Test
void testGetStatistics_shouldCreateNew_whenNotExists() {
// Given
UUID newProjectId = UUID.randomUUID();
when(statisticsRepository.findByProjectId(newProjectId)).thenReturn(Optional.empty());
when(statisticsRepository.save(any(ProjectStatistics.class))).thenAnswer(inv -> inv.getArgument(0));
// When
ProjectStatistics result = statisticsService.getStatistics(newProjectId);
// Then
assertNotNull(result);
assertEquals(0, result.getMemberCount());
verify(statisticsRepository).save(any(ProjectStatistics.class));
}
@Test
void testSyncMemberStatistics_shouldUpdateCount() {
// Given
int newCount = 15;
when(statisticsRepository.findByProjectId(projectId)).thenReturn(Optional.of(testStatistics));
when(statisticsRepository.save(any(ProjectStatistics.class))).thenAnswer(inv -> inv.getArgument(0));
// When
statisticsService.syncMemberStatistics(projectId, newCount);
// Then
verify(statisticsRepository).save(argThat(stats ->
stats.getMemberCount() == newCount &&
stats.getLastSyncedAt() != null
));
}
@Test
void testIncrementMemberCount_shouldIncrement() {
// Given
when(statisticsRepository.findByProjectId(projectId)).thenReturn(Optional.of(testStatistics));
when(statisticsRepository.save(any(ProjectStatistics.class))).thenAnswer(inv -> inv.getArgument(0));
// When
statisticsService.incrementMemberCount(projectId);
// Then
verify(statisticsRepository).save(argThat(stats ->
stats.getMemberCount() == 11
));
}
@Test
void testDecrementMemberCount_shouldDecrement() {
// Given
when(statisticsRepository.findByProjectId(projectId)).thenReturn(Optional.of(testStatistics));
when(statisticsRepository.save(any(ProjectStatistics.class))).thenAnswer(inv -> inv.getArgument(0));
// When
statisticsService.decrementMemberCount(projectId);
// Then
verify(statisticsRepository).save(argThat(stats ->
stats.getMemberCount() == 9
));
}
@Test
void testDecrementMemberCount_shouldNotGoNegative() {
// Given
testStatistics.setMemberCount(0);
when(statisticsRepository.findByProjectId(projectId)).thenReturn(Optional.of(testStatistics));
when(statisticsRepository.save(any(ProjectStatistics.class))).thenAnswer(inv -> inv.getArgument(0));
// When
statisticsService.decrementMemberCount(projectId);
// Then
verify(statisticsRepository).save(argThat(stats ->
stats.getMemberCount() == 0
));
}
}

View File

@ -0,0 +1,99 @@
-- Ether PMS Database Migration Script
-- Version: 2.1
-- Description: Add project statistics, config and status history tables
-- ============================================
-- User Project Relation Table (if not exists)
-- ============================================
CREATE TABLE IF NOT EXISTS user_project (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES mdm_project(id) ON DELETE CASCADE,
role_in_project VARCHAR(20) DEFAULT 'member',
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, project_id)
);
CREATE INDEX IF NOT EXISTS idx_user_project_user ON user_project(user_id);
CREATE INDEX IF NOT EXISTS idx_user_project_project ON user_project(project_id);
-- ============================================
-- Project Statistics Table
-- ============================================
CREATE TABLE IF NOT EXISTS mdm_project_statistics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL UNIQUE REFERENCES mdm_project(id) ON DELETE CASCADE,
member_count INTEGER DEFAULT 0,
building_count INTEGER DEFAULT 0,
unit_count INTEGER DEFAULT 0,
room_count INTEGER DEFAULT 0,
owner_count INTEGER DEFAULT 0,
tenant_count INTEGER DEFAULT 0,
last_synced_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_project_statistics_project ON mdm_project_statistics(project_id);
-- ============================================
-- Project Config Table
-- ============================================
CREATE TABLE IF NOT EXISTS mdm_project_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL UNIQUE REFERENCES mdm_project(id) ON DELETE CASCADE,
enable_reservation BOOLEAN DEFAULT FALSE,
enable_visitor BOOLEAN DEFAULT FALSE,
enable_complaint BOOLEAN DEFAULT TRUE,
enable_payment BOOLEAN DEFAULT FALSE,
enable_announcement BOOLEAN DEFAULT TRUE,
enable_survey BOOLEAN DEFAULT FALSE,
enable_vote BOOLEAN DEFAULT FALSE,
enable_maintenance BOOLEAN DEFAULT TRUE,
enable_asset BOOLEAN DEFAULT FALSE,
custom_config TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_project_config_project ON mdm_project_config(project_id);
-- ============================================
-- Project Status History Table
-- ============================================
CREATE TABLE IF NOT EXISTS mdm_project_status_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES mdm_project(id) ON DELETE CASCADE,
from_status VARCHAR(20),
to_status VARCHAR(20) NOT NULL,
reason VARCHAR(500),
operator_id UUID REFERENCES auth_user(id),
operator_name VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_project_status_history_project ON mdm_project_status_history(project_id);
CREATE INDEX IF NOT EXISTS idx_project_status_history_created ON mdm_project_status_history(created_at);
-- ============================================
-- Project Code Sequence Table
-- ============================================
CREATE TABLE IF NOT EXISTS mdm_project_code_sequence (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
prefix VARCHAR(10) NOT NULL,
current_sequence INTEGER DEFAULT 0,
year INTEGER NOT NULL,
UNIQUE(prefix, year)
);
-- ============================================
-- Update Project Status
-- ============================================
-- Add DRAFT status if not present
UPDATE mdm_project SET status = 'ACTIVE' WHERE status IS NULL;

58
sql/audit_log.sql Normal file
View File

@ -0,0 +1,58 @@
-- 审计日志表
CREATE TABLE IF NOT EXISTS sys_audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID,
username VARCHAR(64) NOT NULL,
operation VARCHAR(128) NOT NULL,
module VARCHAR(64) NOT NULL,
action VARCHAR(64) NOT NULL,
target_type VARCHAR(64),
target_id VARCHAR(64),
content TEXT,
params TEXT,
result TEXT,
ip_address VARCHAR(64),
user_agent VARCHAR(512),
request_url VARCHAR(512),
request_method VARCHAR(16),
execution_time_ms INTEGER,
status VARCHAR(16) DEFAULT 'SUCCESS',
error_msg TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
tenant_id UUID
);
-- 创建索引
CREATE INDEX idx_audit_log_created_at ON sys_audit_log(created_at);
CREATE INDEX idx_audit_log_user_id ON sys_audit_log(user_id);
CREATE INDEX idx_audit_log_module ON sys_audit_log(module);
CREATE INDEX idx_audit_log_action ON sys_audit_log(action);
CREATE INDEX idx_audit_log_target ON sys_audit_log(target_type, target_id);
-- 归档表结构相同用于存储超过90天的数据
CREATE TABLE IF NOT EXISTS sys_audit_log_archive (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID,
username VARCHAR(64) NOT NULL,
operation VARCHAR(128) NOT NULL,
module VARCHAR(64) NOT NULL,
action VARCHAR(64) NOT NULL,
target_type VARCHAR(64),
target_id VARCHAR(64),
content TEXT,
params TEXT,
result TEXT,
ip_address VARCHAR(64),
user_agent VARCHAR(512),
request_url VARCHAR(512),
request_method VARCHAR(16),
execution_time_ms INTEGER,
status VARCHAR(16) DEFAULT 'SUCCESS',
error_msg TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
tenant_id UUID
);
-- 为归档表创建索引
CREATE INDEX idx_audit_archive_created_at ON sys_audit_log_archive(created_at);
CREATE INDEX idx_audit_archive_user_id ON sys_audit_log_archive(user_id);

View File

@ -1,5 +1,6 @@
-- Ether PMS Database Initialization Script -- Ether PMS Database Initialization Script
-- Database: ether_pms -- Database: ether_pms
-- Version: 2.0
-- ============================================ -- ============================================
-- Auth Module Tables -- Auth Module Tables
@ -23,9 +24,9 @@ CREATE TABLE IF NOT EXISTS auth_user (
created_by UUID created_by UUID
); );
CREATE INDEX idx_auth_user_username ON auth_user(username); CREATE INDEX IF NOT EXISTS idx_auth_user_username ON auth_user(username);
CREATE INDEX idx_auth_user_phone ON auth_user(phone); CREATE INDEX IF NOT EXISTS idx_auth_user_phone ON auth_user(phone);
CREATE INDEX idx_auth_user_status ON auth_user(status); CREATE INDEX IF NOT EXISTS idx_auth_user_status ON auth_user(status);
-- Role Table -- Role Table
CREATE TABLE IF NOT EXISTS auth_role ( CREATE TABLE IF NOT EXISTS auth_role (
@ -41,8 +42,8 @@ CREATE TABLE IF NOT EXISTS auth_role (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_auth_role_code ON auth_role(code); CREATE INDEX IF NOT EXISTS idx_auth_role_code ON auth_role(code);
CREATE INDEX idx_auth_role_project ON auth_role(project_id); CREATE INDEX IF NOT EXISTS idx_auth_role_project ON auth_role(project_id);
-- Permission Table -- Permission Table
CREATE TABLE IF NOT EXISTS auth_permission ( CREATE TABLE IF NOT EXISTS auth_permission (
@ -59,9 +60,9 @@ CREATE TABLE IF NOT EXISTS auth_permission (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_auth_permission_code ON auth_permission(code); CREATE INDEX IF NOT EXISTS idx_auth_permission_code ON auth_permission(code);
CREATE INDEX idx_auth_permission_type ON auth_permission(type); CREATE INDEX IF NOT EXISTS idx_auth_permission_type ON auth_permission(type);
CREATE INDEX idx_auth_permission_parent ON auth_permission(parent_code); CREATE INDEX IF NOT EXISTS idx_auth_permission_parent ON auth_permission(parent_code);
-- User-Role Relation Table -- User-Role Relation Table
CREATE TABLE IF NOT EXISTS auth_user_role ( CREATE TABLE IF NOT EXISTS auth_user_role (
@ -106,8 +107,8 @@ CREATE TABLE IF NOT EXISTS mdm_project (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_mdm_project_code ON mdm_project(code); CREATE INDEX IF NOT EXISTS idx_mdm_project_code ON mdm_project(code);
CREATE INDEX idx_mdm_project_status ON mdm_project(status); CREATE INDEX IF NOT EXISTS idx_mdm_project_status ON mdm_project(status);
-- Space Node Table -- Space Node Table
CREATE TABLE IF NOT EXISTS mdm_space_node ( CREATE TABLE IF NOT EXISTS mdm_space_node (
@ -129,46 +130,99 @@ CREATE TABLE IF NOT EXISTS mdm_space_node (
UNIQUE(code, project_code) UNIQUE(code, project_code)
); );
CREATE INDEX idx_mdm_space_node_project ON mdm_space_node(project_code); CREATE INDEX IF NOT EXISTS idx_mdm_space_node_project ON mdm_space_node(project_code);
CREATE INDEX idx_mdm_space_node_type ON mdm_space_node(node_type); CREATE INDEX IF NOT EXISTS idx_mdm_space_node_type ON mdm_space_node(node_type);
CREATE INDEX idx_mdm_space_node_parent ON mdm_space_node(parent_code); CREATE INDEX IF NOT EXISTS idx_mdm_space_node_parent ON mdm_space_node(parent_code);
-- ============================================ -- ============================================
-- Initial Data -- Initial Data
-- ============================================ -- ============================================
-- Insert default admin user -- Insert default admin user
-- Password: Admin123! (BCrypt encrypted) -- Password: Admin123! (BCrypt encrypted)
-- Password requirements: 8-20 chars, uppercase, lowercase, digit, special char -- Password requirements: 8-20 chars, uppercase, lowercase, digit, special char
INSERT INTO auth_user (username, password, real_name, status) INSERT INTO auth_user (username, password, real_name, status)
VALUES ('admin', '$2a$10$N9qo8uLOickgx2ZMRZoMye/U.N4.5F.HQW5R.HGmh3R1VJfF5WQa', '系统管理员', 'ACTIVE'); VALUES ('admin', '$2a$10$N9qo8uLOickgx2ZMRZoMye/U.N4.5F.HQW5R.HGmh3R1VJfF5WQa', '系统管理员', 'ACTIVE')
ON CONFLICT (username) DO NOTHING;
-- Insert default roles -- Insert default roles
INSERT INTO auth_role (code, name, description, type, data_scope, status) INSERT INTO auth_role (code, name, description, type, data_scope, status)
VALUES VALUES
('SYSTEM_ADMIN', '系统管理员', '系统超级管理员', 'SYSTEM', 'ALL', 'ENABLED'), ('SYSTEM_ADMIN', '系统管理员', '系统超级管理员', 'SYSTEM', 'ALL', 'ENABLED'),
('PROJECT_ADMIN', '项目管理员', '项目管理员', 'PROJECT', 'PROJECT', 'ENABLED'), ('PROJECT_ADMIN', '项目管理员', '项目管理员', 'PROJECT', 'PROJECT', 'ENABLED'),
('EMPLOYEE', '普通员工', '普通员工', 'DEPARTMENT', 'SELF', 'ENABLED'); ('EMPLOYEE', '普通员工', '普通员工', 'DEPARTMENT', 'SELF', 'ENABLED')
ON CONFLICT (code) DO NOTHING;
-- Insert default permissions -- Insert comprehensive permissions
INSERT INTO auth_permission (code, name, type, resource, method, sort_order) -- Format: module:action (e.g., dashboard:view, system:menu)
VALUES INSERT INTO auth_permission (code, name, type, resource, method, description, sort_order)
('dashboard', '仪表盘', 'MENU', '/dashboard', 'GET', 1), VALUES
('user:list', '用户列表', 'BUTTON', '/api/users', 'GET', 10), -- Dashboard / 仪表盘
('user:create', '创建用户', 'BUTTON', '/api/users', 'POST', 11), ('dashboard:view', '查看仪表盘', 'MENU', '/dashboard', 'GET', '查看仪表盘', 1),
('user:update', '更新用户', 'BUTTON', '/api/users', 'PUT', 12),
('user:delete', '删除用户', 'BUTTON', '/api/users', 'DELETE', 13), -- System Management / 系统管理
('role:list', '角色列表', 'BUTTON', '/api/roles', 'GET', 20), ('system:menu', '系统管理', 'MENU', '/system', 'GET', '系统管理菜单', 99),
('role:create', '创建角色', 'BUTTON', '/api/roles', 'POST', 21), ('system:user:list', '用户列表', 'BUTTON', '/api/users', 'GET', '查看用户列表', 100),
('role:update', '更新角色', 'BUTTON', '/api/roles', 'PUT', 22), ('system:user:create', '创建用户', 'BUTTON', '/api/users', 'POST', '创建新用户', 101),
('role:delete', '删除角色', 'BUTTON', '/api/roles', 'DELETE', 23), ('system:user:update', '更新用户', 'BUTTON', '/api/users', 'PUT', '更新用户信息', 102),
('project:list', '项目列表', 'BUTTON', '/api/projects', 'GET', 30), ('system:user:delete', '删除用户', 'BUTTON', '/api/users', 'DELETE', '删除用户', 103),
('project:create', '创建项目', 'BUTTON', '/api/projects', 'POST', 31), ('system:user:resetPwd', '重置密码', 'BUTTON', '/api/users/*/reset-password', 'POST', '重置用户密码', 104),
('project:update', '更新项目', 'BUTTON', '/api/projects', 'PUT', 32), ('system:user:export', '导出用户', 'BUTTON', '/api/users/export', 'GET', '导出用户数据', 105),
('project:delete', '删除项目', 'BUTTON', '/api/projects', 'DELETE', 33);
-- Role Management / 角色管理
('system:role:list', '角色列表', 'BUTTON', '/api/roles', 'GET', '查看角色列表', 200),
('system:role:create', '创建角色', 'BUTTON', '/api/roles', 'POST', '创建新角色', 201),
('system:role:update', '更新角色', 'BUTTON', '/api/roles', 'PUT', '更新角色信息', 202),
('system:role:delete', '删除角色', 'BUTTON', '/api/roles', 'DELETE', '删除角色', 203),
('system:role:assignPermissions', '分配权限', 'BUTTON', '/api/roles/*/permissions', 'POST', '为角色分配权限', 204),
-- Permission Management / 权限管理
('system:permission:list', '权限列表', 'BUTTON', '/api/permissions', 'GET', '查看权限列表', 300),
('system:permission:create', '创建权限', 'BUTTON', '/api/permissions', 'POST', '创建新权限', 301),
('system:permission:update', '更新权限', 'BUTTON', '/api/permissions', 'PUT', '更新权限信息', 302),
('system:permission:delete', '删除权限', 'BUTTON', '/api/permissions', 'DELETE', '删除权限', 303),
-- Project Management / 项目管理
('project:list', '项目列表', 'MENU', '/api/projects', 'GET', '查看项目列表', 400),
('project:create', '创建项目', 'BUTTON', '/api/projects', 'POST', '创建新项目', 401),
('project:update', '更新项目', 'BUTTON', '/api/projects', 'PUT', '更新项目信息', 402),
('project:delete', '删除项目', 'BUTTON', '/api/projects', 'DELETE', '删除项目', 403),
('project:detail', '项目详情', 'BUTTON', '/api/projects/*', 'GET', '查看项目详情', 404),
-- Space Management / 空间管理
('space:list', '空间列表', 'MENU', '/api/spaces', 'GET', '查看空间列表', 500),
('space:create', '创建空间', 'BUTTON', '/api/spaces', 'POST', '创建新空间', 501),
('space:update', '更新空间', 'BUTTON', '/api/spaces', 'PUT', '更新空间信息', 502),
('space:delete', '删除空间', 'BUTTON', '/api/spaces', 'DELETE', '删除空间', 503),
('space:import', '导入空间', 'BUTTON', '/api/spaces/import', 'POST', '批量导入空间', 504),
('space:export', '导出空间', 'BUTTON', '/api/spaces/export', 'GET', '导出空间数据', 505),
-- Asset Management / 资产管理
('asset:list', '资产列表', 'MENU', '/api/assets', 'GET', '查看资产列表', 600),
('asset:create', '创建资产', 'BUTTON', '/api/assets', 'POST', '创建新资产', 601),
('asset:update', '更新资产', 'BUTTON', '/api/assets', 'PUT', '更新资产信息', 602),
('asset:delete', '删除资产', 'BUTTON', '/api/assets', 'DELETE', '删除资产', 603),
('asset:transfer', '资产调拨', 'BUTTON', '/api/assets/transfer', 'POST', '资产调拨', 604),
('asset:maintain', '资产维护', 'BUTTON', '/api/assets/maintain', 'POST', '资产维护记录', 605),
-- Audit / 审计管理
('audit:view', '查看审计日志', 'MENU', '/api/audit', 'GET', '查看审计日志', 700),
('audit:export', '导出审计日志', 'BUTTON', '/api/audit/export', 'GET', '导出审计日志', 701),
-- Finance / 财务管理
('finance:list', '财务列表', 'MENU', '/api/finance', 'GET', '查看财务列表', 800),
('finance:create', '创建财务记录', 'BUTTON', '/api/finance', 'POST', '创建财务记录', 801),
('finance:update', '更新财务记录', 'BUTTON', '/api/finance', 'PUT', '更新财务记录', 802),
('finance:delete', '删除财务记录', 'BUTTON', '/api/finance', 'DELETE', '删除财务记录', 803),
('finance:report', '财务报表', 'BUTTON', '/api/finance/report', 'GET', '生成财务报表', 804)
ON CONFLICT (code) DO NOTHING;
-- Assign all permissions to SYSTEM_ADMIN role -- Assign all permissions to SYSTEM_ADMIN role
INSERT INTO auth_role_permission (role_id, permission_id) INSERT INTO auth_role_permission (role_id, permission_id)
SELECT r.id, p.id SELECT r.id, p.id
FROM auth_role r, auth_permission p FROM auth_role r, auth_permission p
WHERE r.code = 'SYSTEM_ADMIN'; WHERE r.code = 'SYSTEM_ADMIN'
AND NOT EXISTS (
SELECT 1 FROM auth_role_permission rp
WHERE rp.role_id = r.id AND rp.permission_id = p.id
);

63
sql/reset_auth.sql Normal file
View File

@ -0,0 +1,63 @@
BEGIN;
ALTER TABLE biz_data_access DISABLE TRIGGER ALL;
DELETE FROM auth_role_permission;
DELETE FROM auth_permission;
DELETE FROM auth_user_role;
DELETE FROM auth_role;
DELETE FROM auth_user;
INSERT INTO auth_user (username, password, real_name, status)
VALUES ('admin', '$2a$10$2JRCyrbZANZdGD4sgplVjuIOPvK1P/Be1/4iwXwkUqpbEDo2AHcuC', '系统管理员', 'ACTIVE');
INSERT INTO auth_role (code, name, description, type, data_scope, status)
VALUES
('SYSTEM_ADMIN', '系统管理员', '系统超级管理员', 'SYSTEM', 'ALL', 'ENABLED'),
('PROJECT_ADMIN', '项目管理员', '项目管理员', 'PROJECT', 'PROJECT', 'ENABLED'),
('EMPLOYEE', '普通员工', '普通员工', 'DEPARTMENT', 'SELF', 'ENABLED');
INSERT INTO auth_permission (code, name, type, resource, method, description, sort_order) VALUES
('dashboard:view', '查看仪表盘', 'MENU', '/dashboard', 'GET', '查看仪表盘', 1),
('system:user:menu', '用户管理', 'MENU', '/system/users', 'GET', '用户管理菜单', 101),
('system:role:menu', '角色管理', 'MENU', '/system/roles', 'GET', '角色管理菜单', 201),
('system:user:list', '用户列表', 'BUTTON', '/api/users', 'GET', '查看用户列表', 100),
('system:user:create', '创建用户', 'BUTTON', '/api/users', 'POST', '创建新用户', 101),
('system:user:update', '更新用户', 'BUTTON', '/api/users', 'PUT', '更新用户信息', 102),
('system:user:delete', '删除用户', 'BUTTON', '/api/users', 'DELETE', '删除用户', 103),
('system:role:list', '角色列表', 'BUTTON', '/api/roles', 'GET', '查看角色列表', 200),
('system:role:create', '创建角色', 'BUTTON', '/api/roles', 'POST', '创建新角色', 201),
('system:role:update', '更新角色', 'BUTTON', '/api/roles', 'PUT', '更新角色信息', 202),
('system:role:delete', '删除角色', 'BUTTON', '/api/roles', 'DELETE', '删除角色', 203),
('system:role:assignPermissions', '分配权限', 'BUTTON', '/api/roles/*/permissions', 'POST', '为角色分配权限', 204),
('system:permission:list', '权限列表', 'BUTTON', '/api/permissions', 'GET', '查看权限列表', 300),
('system:permission:create', '创建权限', 'BUTTON', '/api/permissions', 'POST', '创建新权限', 301),
('system:permission:update', '更新权限', 'BUTTON', '/api/permissions', 'PUT', '更新权限信息', 302),
('system:permission:delete', '删除权限', 'BUTTON', '/api/permissions', 'DELETE', '删除权限', 303),
('project:list', '项目列表', 'MENU', '/api/projects', 'GET', '查看项目列表', 400),
('project:create', '创建项目', 'BUTTON', '/api/projects', 'POST', '创建新项目', 401),
('project:update', '更新项目', 'BUTTON', '/api/projects', 'PUT', '更新项目信息', 402),
('project:delete', '删除项目', 'BUTTON', '/api/projects', 'DELETE', '删除项目', 403),
('space:list', '空间列表', 'MENU', '/api/spaces', 'GET', '查看空间列表', 500),
('space:create', '创建空间', 'BUTTON', '/api/spaces', 'POST', '创建新空间', 501),
('space:update', '更新空间', 'BUTTON', '/api/spaces', 'PUT', '更新空间信息', 502),
('space:delete', '删除空间', 'BUTTON', '/api/spaces', 'DELETE', '删除空间', 503),
('asset:list', '资产列表', 'MENU', '/api/assets', 'GET', '查看资产列表', 600),
('asset:create', '创建资产', 'BUTTON', '/api/assets', 'POST', '创建新资产', 601),
('asset:update', '更新资产', 'BUTTON', '/api/assets', 'PUT', '更新资产信息', 602),
('asset:delete', '删除资产', 'BUTTON', '/api/assets', 'DELETE', '删除资产', 603),
('audit:view', '查看审计日志', 'MENU', '/api/audit', 'GET', '查看审计日志', 700),
('audit:export', '导出审计日志', 'BUTTON', '/api/audit/export', 'GET', '导出审计日志', 701),
('finance:list', '财务列表', 'MENU', '/api/finance', 'GET', '查看财务列表', 800),
('finance:create', '创建财务记录', 'BUTTON', '/api/finance', 'POST', '创建财务记录', 801),
('finance:update', '更新财务记录', 'BUTTON', '/api/finance', 'PUT', '更新财务记录', 802),
('finance:delete', '删除财务记录', 'BUTTON', '/api/finance', 'DELETE', '删除财务记录', 803);
INSERT INTO auth_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM auth_role r, auth_permission p
WHERE r.code = 'SYSTEM_ADMIN';
ALTER TABLE biz_data_access ENABLE TRIGGER ALL;
COMMIT;