From aa3f6c4cedfd1d0125e5356d6686f653b9668dca Mon Sep 17 00:00:00 2001 From: chiguyong Date: Mon, 23 Mar 2026 23:53:55 +0800 Subject: [PATCH] feat: add equipment extension fields to SpaceNode entity --- HashPassword.class | Bin 0 -> 590 bytes HashPassword.java | 9 + .../pms/auth/annotation/OperationLog.java | 60 ++++ .../ether/pms/auth/aspect/AuditLogAspect.java | 179 +++++++++++ .../ether/pms/auth/config/AsyncConfig.java | 28 ++ .../auth/controller/AuditLogController.java | 94 ++++++ .../pms/auth/controller/RoleController.java | 24 +- .../pms/auth/controller/UserController.java | 15 +- .../auth/controller/dto/RoleWithUsersDTO.java | 53 ++++ .../auth/controller/dto/UserWithRolesDTO.java | 60 ++++ .../com/ether/pms/auth/entity/AuditLog.java | 125 ++++++++ .../java/com/ether/pms/auth/entity/User.java | 3 +- .../auth/repository/AuditLogRepository.java | 50 +++ .../pms/auth/repository/RoleRepository.java | 12 +- .../repository/UserProjectRepository.java | 4 + .../pms/auth/repository/UserRepository.java | 10 + .../auth/scheduler/AuditLogArchiveTask.java | 30 ++ .../pms/auth/service/AuditLogService.java | 138 +++++++++ .../ether/pms/auth/service/RoleService.java | 13 + .../ether/pms/auth/service/UserService.java | 2 +- .../java/com/ether/pms/common/ErrorCode.java | 5 + module-mdm/pom.xml | 19 ++ .../pms/mdm/controller/ProjectController.java | 143 ++++++++- .../mdm/controller/SpaceNodeController.java | 65 ++-- .../ether/pms/mdm/dto/AddMemberRequest.java | 31 ++ .../pms/mdm/dto/ChangeStatusRequest.java | 27 ++ .../com/ether/pms/mdm/dto/PageResponse.java | 43 +++ .../ether/pms/mdm/dto/ProjectConfigDTO.java | 70 +++++ .../ether/pms/mdm/dto/ProjectMemberDTO.java | 57 ++++ .../pms/mdm/dto/ProjectQueryRequest.java | 22 ++ .../pms/mdm/dto/ProjectSelectorItem.java | 28 ++ .../ether/pms/mdm/dto/SpaceNodeCreateDTO.java | 71 +++++ .../ether/pms/mdm/dto/SpaceNodeTreeDTO.java | 55 ++++ .../ether/pms/mdm/dto/SpaceNodeUpdateDTO.java | 57 ++++ .../com/ether/pms/mdm/entity/Project.java | 22 +- .../ether/pms/mdm/entity/ProjectConfig.java | 69 +++++ .../pms/mdm/entity/ProjectStatistics.java | 78 +++++ .../pms/mdm/entity/ProjectStatusHistory.java | 45 +++ .../com/ether/pms/mdm/entity/SpaceNode.java | 289 ++++++++++++++---- .../ether/pms/mdm/enums/ProjectStatus.java | 59 ++++ .../repository/ProjectConfigRepository.java | 18 ++ .../pms/mdm/repository/ProjectRepository.java | 31 +- .../ProjectStatisticsRepository.java | 18 ++ .../ProjectStatusHistoryRepository.java | 26 ++ .../mdm/repository/SpaceNodeRepository.java | 25 +- .../pms/mdm/service/ProjectConfigService.java | 130 ++++++++ .../pms/mdm/service/ProjectMemberService.java | 138 +++++++++ .../ether/pms/mdm/service/ProjectService.java | 148 ++++++++- .../mdm/service/ProjectStatisticsService.java | 113 +++++++ .../pms/mdm/service/SpaceNodeService.java | 228 +++++++++++--- .../ether/pms/mdm/entity/SpaceNodeTest.java | 157 ++++++++++ .../mdm/service/ProjectMemberServiceTest.java | 203 ++++++++++++ .../pms/mdm/service/ProjectServiceTest.java | 216 +++++++++++++ .../service/ProjectStatisticsServiceTest.java | 140 +++++++++ sql/V2.1__project_enhancements.sql | 99 ++++++ sql/audit_log.sql | 58 ++++ sql/init.sql | 126 +++++--- sql/reset_auth.sql | 63 ++++ 58 files changed, 3897 insertions(+), 204 deletions(-) create mode 100644 HashPassword.class create mode 100644 HashPassword.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/annotation/OperationLog.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/aspect/AuditLogAspect.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/config/AsyncConfig.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/controller/AuditLogController.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/controller/dto/RoleWithUsersDTO.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserWithRolesDTO.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/entity/AuditLog.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/repository/AuditLogRepository.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/scheduler/AuditLogArchiveTask.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/service/AuditLogService.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/dto/AddMemberRequest.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/dto/ChangeStatusRequest.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/dto/PageResponse.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectConfigDTO.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectMemberDTO.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectQueryRequest.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectSelectorItem.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeCreateDTO.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeTreeDTO.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeUpdateDTO.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectConfig.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectStatistics.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectStatusHistory.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/enums/ProjectStatus.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectConfigRepository.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectStatisticsRepository.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectStatusHistoryRepository.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectConfigService.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectMemberService.java create mode 100644 module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectStatisticsService.java create mode 100644 module-mdm/src/test/java/com/ether/pms/mdm/entity/SpaceNodeTest.java create mode 100644 module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectMemberServiceTest.java create mode 100644 module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectServiceTest.java create mode 100644 module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectStatisticsServiceTest.java create mode 100644 sql/V2.1__project_enhancements.sql create mode 100644 sql/audit_log.sql create mode 100644 sql/reset_auth.sql diff --git a/HashPassword.class b/HashPassword.class new file mode 100644 index 0000000000000000000000000000000000000000..1ba0fb8d6f109daa9d168c92176f87deb61c61a4 GIT binary patch literal 590 zcmZWm%T60X5UlotwU;FZ6U;l|b+7?tMRGJmfCb`U2$CZt#A&=9%z$?nW_LmUEOG!T zNPGYvh04^Rnt$;e;)yCV97=bX$u(#Squ<{&&9QHJ0fViznbULiwOhk zDp2uHLb_ZzvXCQehPv%Wms$nwQ!RXX6Y2{$l3t=!+;u(Oy^KS**^BOW&0G&f6fvr` z6L?`uY8w{PISdl=TPb3!@G`W9);KMmLJ3&kn+b4UN4}Rp=fvFJ`tBe$~b}CM--kn8Gw+wD;~u48by{ z<4(X$`jx#3FyAvvaP~xW_U4$7sj+23p{@dXkoZlhe~V^^GZ|kf?o=wD{O_f5M6erS zqCL5*40xo!={F+|^O)h`nUw;W@$vqJZ?hbUV|Mu-bjt_ASKdt-NF$FqroKR{X8N2R qe}Hv-k9>WlXy3uv|AXPxJCxqZMPwM3MULAPcv2-k5f(U?!s0(xS&0e& literal 0 HcmV?d00001 diff --git a/HashPassword.java b/HashPassword.java new file mode 100644 index 0000000..8b884dd --- /dev/null +++ b/HashPassword.java @@ -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); + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/annotation/OperationLog.java b/module-auth/src/main/java/com/ether/pms/auth/annotation/OperationLog.java new file mode 100644 index 0000000..c95fc10 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/annotation/OperationLog.java @@ -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"}; +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/aspect/AuditLogAspect.java b/module-auth/src/main/java/com/ether/pms/auth/aspect/AuditLogAspect.java new file mode 100644 index 0000000..9b349e0 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/aspect/AuditLogAspect.java @@ -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 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 "[]"; + } + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/config/AsyncConfig.java b/module-auth/src/main/java/com/ether/pms/auth/config/AsyncConfig.java new file mode 100644 index 0000000..83d1e93 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/config/AsyncConfig.java @@ -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; + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/AuditLogController.java b/module-auth/src/main/java/com/ether/pms/auth/controller/AuditLogController.java new file mode 100644 index 0000000..3a2b96e --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/AuditLogController.java @@ -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> 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 result = auditLogService.searchLogs(module, action, username, startDate, endDate, pageable); + + return ApiResponse.success(result); + } + + /** + * 获取模块列表(用于筛选) + */ + @GetMapping("/modules") + public ApiResponse>> getModules() { + List> 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>> getActions() { + List> 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> getStats() { + long count = auditLogService.getRecentLogCount(); + return ApiResponse.success(Map.of( + "total", count, + "retentionDays", 30, + "description", "最近30天的审计日志数量" + )); + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/RoleController.java b/module-auth/src/main/java/com/ether/pms/auth/controller/RoleController.java index 18344f7..ff0eb95 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/controller/RoleController.java +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/RoleController.java @@ -1,6 +1,10 @@ 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.User; import com.ether.pms.auth.service.RoleService; import com.ether.pms.common.ApiResponse; import lombok.RequiredArgsConstructor; @@ -33,26 +37,40 @@ public class RoleController { } @PostMapping + @OperationLog(operation = "创建角色", module = "ROLE", action = AuditLog.ActionType.CREATE) public ResponseEntity> create(@RequestBody Role role) { return ResponseEntity.ok(ApiResponse.success(roleService.create(role))); } - + @PutMapping("/{id}") + @OperationLog(operation = "更新角色", module = "ROLE", action = AuditLog.ActionType.UPDATE) public ResponseEntity> update(@PathVariable UUID id, @RequestBody Role role) { return ResponseEntity.ok(ApiResponse.success(roleService.update(id, role))); } - + @DeleteMapping("/{id}") + @OperationLog(operation = "删除角色", module = "ROLE", action = AuditLog.ActionType.DELETE) public ResponseEntity> delete(@PathVariable UUID id) { roleService.delete(id); return ResponseEntity.ok(ApiResponse.success()); } - + @PostMapping("/{id}/permissions") + @OperationLog(operation = "分配权限", module = "ROLE", action = AuditLog.ActionType.ASSIGN) public ResponseEntity> assignPermissions( @PathVariable UUID id, @RequestBody List permissionIds) { roleService.assignPermissions(id, permissionIds); return ResponseEntity.ok(ApiResponse.success()); } + + @GetMapping("/{id}/permissions") + public ResponseEntity>> getPermissions(@PathVariable UUID id) { + return ResponseEntity.ok(ApiResponse.success(roleService.getPermissions(id))); + } + + @GetMapping("/{id}/users") + public ResponseEntity>> getUsersByRoleId(@PathVariable UUID id) { + return ResponseEntity.ok(ApiResponse.success(roleService.getUsersByRoleId(id))); + } } diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/UserController.java b/module-auth/src/main/java/com/ether/pms/auth/controller/UserController.java index 75807f5..10bb161 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/controller/UserController.java +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/UserController.java @@ -1,6 +1,8 @@ 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.entity.AuditLog; import com.ether.pms.auth.entity.User; import com.ether.pms.auth.entity.UserProject; import com.ether.pms.auth.service.UserProjectService; @@ -33,30 +35,35 @@ public class UserController { } @PostMapping + @OperationLog(operation = "创建用户", module = "USER", action = AuditLog.ActionType.CREATE) public ResponseEntity> create(@RequestBody User user) { return ResponseEntity.ok(ApiResponse.success(userService.create(user))); } - + @PutMapping("/{id}") + @OperationLog(operation = "更新用户", module = "USER", action = AuditLog.ActionType.UPDATE) public ResponseEntity> update(@PathVariable UUID id, @RequestBody User user) { return ResponseEntity.ok(ApiResponse.success(userService.update(id, user))); } - + @DeleteMapping("/{id}") + @OperationLog(operation = "删除用户", module = "USER", action = AuditLog.ActionType.DELETE) public ResponseEntity> delete(@PathVariable UUID id) { userService.delete(id); return ResponseEntity.ok(ApiResponse.success()); } - + @PutMapping("/{id}/password") + @OperationLog(operation = "修改密码", module = "USER", action = AuditLog.ActionType.UPDATE) public ResponseEntity> updatePassword( @PathVariable UUID id, @RequestBody PasswordRequest request) { userService.updatePassword(id, request.getOldPassword(), request.getNewPassword()); return ResponseEntity.ok(ApiResponse.success()); } - + @PostMapping("/{id}/roles") + @OperationLog(operation = "分配角色", module = "USER", action = AuditLog.ActionType.ASSIGN) public ResponseEntity> assignRoles( @PathVariable UUID id, @RequestBody List roleIds) { diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/RoleWithUsersDTO.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/RoleWithUsersDTO.java new file mode 100644 index 0000000..fbefe29 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/RoleWithUsersDTO.java @@ -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 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; + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserWithRolesDTO.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserWithRolesDTO.java new file mode 100644 index 0000000..8bd1118 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserWithRolesDTO.java @@ -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 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; + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/AuditLog.java b/module-auth/src/main/java/com/ether/pms/auth/entity/AuditLog.java new file mode 100644 index 0000000..e75bda4 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/AuditLog.java @@ -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; + } + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/User.java b/module-auth/src/main/java/com/ether/pms/auth/entity/User.java index 5b72dad..ed12425 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/entity/User.java +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/User.java @@ -26,8 +26,7 @@ public class User { private String username; @NotNull(message = "密码不能为空") - @Size(min = 8, max = 20, message = "密码长度必须在8-20位之间") - @Column(nullable = false) + @Column(nullable = false, length = 255) private String password; private String salt; diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/AuditLogRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/AuditLogRepository.java new file mode 100644 index 0000000..d3bac43 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/AuditLogRepository.java @@ -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, JpaSpecificationExecutor { + + /** + * 查询最近30天的审计日志(分页) + */ + @Query("SELECT a FROM AuditLog a WHERE a.createdAt >= :startTime ORDER BY a.createdAt DESC") + Page findRecentLogs(@Param("startTime") LocalDateTime startTime, Pageable pageable); + + /** + * 查询最近30天的审计日志(不分页) + */ + @Query("SELECT a FROM AuditLog a WHERE a.createdAt >= :startTime ORDER BY a.createdAt DESC") + List 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 findByCreatedAtBefore(@Param("cutoffTime") LocalDateTime cutoffTime); + + /** + * 统计最近30天的日志数量 + */ + @Query("SELECT COUNT(a) FROM AuditLog a WHERE a.createdAt >= :startTime") + long countRecentLogs(@Param("startTime") LocalDateTime startTime); +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/RoleRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/RoleRepository.java index 3244b35..bafb3c9 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/repository/RoleRepository.java +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/RoleRepository.java @@ -1,6 +1,7 @@ package com.ether.pms.auth.repository; import com.ether.pms.auth.entity.Role; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; @@ -9,12 +10,15 @@ import java.util.UUID; @Repository public interface RoleRepository extends JpaRepository { - + Optional findByCode(String code); - + boolean existsByCode(String code); - + List findByProjectId(String projectId); - + List findByType(Role.RoleType type); + + @EntityGraph(attributePaths = {"permissions"}) + Optional findWithPermissionsById(UUID id); } diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/UserProjectRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/UserProjectRepository.java index 9521057..bca1003 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/repository/UserProjectRepository.java +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/UserProjectRepository.java @@ -1,6 +1,8 @@ package com.ether.pms.auth.repository; 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.Query; import org.springframework.data.repository.query.Param; @@ -15,6 +17,8 @@ public interface UserProjectRepository extends JpaRepository List findByProjectId(UUID projectId); + Page findByProjectId(UUID projectId, Pageable pageable); + @Query("SELECT up.projectId FROM UserProject up WHERE up.userId = :userId") List findProjectIdsByUserId(@Param("userId") UUID userId); diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/UserRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/UserRepository.java index 1032e45..d831b40 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/repository/UserRepository.java +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/UserRepository.java @@ -5,6 +5,7 @@ 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; @@ -16,6 +17,15 @@ public interface UserRepository extends JpaRepository { @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username") Optional findByUsernameWithRoles(@Param("username") String username); + @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles") + List findAllWithRoles(); + + @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.id = :id") + Optional findByIdWithRoles(@Param("id") UUID id); + + @Query("SELECT u FROM User u JOIN u.roles r WHERE r.id = :roleId") + List findByRoleId(@Param("roleId") UUID roleId); + boolean existsByUsername(String username); boolean existsByPhone(String phone); diff --git a/module-auth/src/main/java/com/ether/pms/auth/scheduler/AuditLogArchiveTask.java b/module-auth/src/main/java/com/ether/pms/auth/scheduler/AuditLogArchiveTask.java new file mode 100644 index 0000000..b784975 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/scheduler/AuditLogArchiveTask.java @@ -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); + } + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/AuditLogService.java b/module-auth/src/main/java/com/ether/pms/auth/service/AuditLogService.java new file mode 100644 index 0000000..4611ac9 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/service/AuditLogService.java @@ -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 findRecentLogs(Pageable pageable) { + LocalDateTime startTime = LocalDateTime.now().minusDays(30); + return auditLogRepository.findRecentLogs(startTime, pageable); + } + + /** + * 条件查询审计日志(最近30天内) + */ + public Page 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 spec = (root, query, cb) -> { + List 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 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); + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/RoleService.java b/module-auth/src/main/java/com/ether/pms/auth/service/RoleService.java index e65152d..e8e2bb0 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/service/RoleService.java +++ b/module-auth/src/main/java/com/ether/pms/auth/service/RoleService.java @@ -2,8 +2,10 @@ package com.ether.pms.auth.service; import com.ether.pms.auth.entity.Role; 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.PermissionRepository; +import com.ether.pms.auth.repository.UserRepository; import com.ether.pms.common.BusinessException; import com.ether.pms.common.ErrorCode; import lombok.RequiredArgsConstructor; @@ -19,6 +21,7 @@ public class RoleService { private final RoleRepository roleRepository; private final PermissionRepository permissionRepository; + private final UserRepository userRepository; public List findAll() { return roleRepository.findAll(); @@ -84,4 +87,14 @@ public class RoleService { public void delete(UUID id) { roleRepository.deleteById(id); } + + public List getPermissions(UUID roleId) { + Role role = roleRepository.findWithPermissionsById(roleId) + .orElseThrow(() -> new BusinessException(ErrorCode.ROLE_002)); + return role.getPermissions(); + } + + public List getUsersByRoleId(UUID roleId) { + return userRepository.findByRoleId(roleId); + } } diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/UserService.java b/module-auth/src/main/java/com/ether/pms/auth/service/UserService.java index 764e9f3..d00ab97 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/service/UserService.java +++ b/module-auth/src/main/java/com/ether/pms/auth/service/UserService.java @@ -23,7 +23,7 @@ public class UserService { private final PasswordService passwordService; public List findAll() { - return userRepository.findAll(); + return userRepository.findAllWithRoles(); } public User findById(UUID id) { diff --git a/module-common/src/main/java/com/ether/pms/common/ErrorCode.java b/module-common/src/main/java/com/ether/pms/common/ErrorCode.java index a434fd4..6546ed2 100644 --- a/module-common/src/main/java/com/ether/pms/common/ErrorCode.java +++ b/module-common/src/main/java/com/ether/pms/common/ErrorCode.java @@ -28,9 +28,14 @@ public enum ErrorCode { PROJECT_001(5001, "项目编码已存在"), PROJECT_002(5002, "项目不存在"), + PROJECT_003(5003, "项目状态转换无效"), + PROJECT_004(5004, "项目成员已存在"), + PROJECT_005(5005, "项目成员不存在"), + PROJECT_006(5006, "项目配置不存在"), SPACE_001(6001, "空间节点编码已存在"), SPACE_002(6002, "空间节点不存在"), + SPACE_003(6003, "空间节点存在子节点,无法删除"), SYSTEM_ERROR(9999, "系统错误"); diff --git a/module-mdm/pom.xml b/module-mdm/pom.xml index f5e151d..37f889c 100644 --- a/module-mdm/pom.xml +++ b/module-mdm/pom.xml @@ -23,6 +23,12 @@ ${project.version} + + com.ether + module-auth + ${project.version} + + org.springframework.boot spring-boot-starter-web @@ -53,5 +59,18 @@ org.mapstruct mapstruct + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + test + diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/controller/ProjectController.java b/module-mdm/src/main/java/com/ether/pms/mdm/controller/ProjectController.java index 0b6d786..35ae085 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/controller/ProjectController.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/controller/ProjectController.java @@ -1,9 +1,22 @@ 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.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.ProjectStatisticsService; import com.ether.pms.common.ApiResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -11,40 +24,150 @@ import java.util.List; import java.util.UUID; @RestController -@RequestMapping("/api/projects") +@RequestMapping("/api/mdm/projects") @RequiredArgsConstructor public class ProjectController { - + private final ProjectService projectService; - + private final ProjectMemberService projectMemberService; + private final ProjectConfigService projectConfigService; + private final ProjectStatisticsService projectStatisticsService; + + /** + * PM-001: 分页查询项目列表 + */ @GetMapping - public ResponseEntity>> findAll() { - return ResponseEntity.ok(ApiResponse.success(projectService.findAll())); + public ResponseEntity>> queryProjects(ProjectQueryRequest request) { + return ResponseEntity.ok(ApiResponse.success(projectService.queryProjects(request))); } - + + /** + * PM-010: 获取项目选择器列表 + */ + @GetMapping("/selector") + public ResponseEntity>> getSelectorList() { + return ResponseEntity.ok(ApiResponse.success(projectService.getSelectorList())); + } + + /** + * PM-005: 生成项目编码 + */ + @GetMapping("/generate-code") + public ResponseEntity> generateCode() { + return ResponseEntity.ok(ApiResponse.success(projectService.generateCode())); + } + @GetMapping("/{id}") public ResponseEntity> findById(@PathVariable UUID id) { return ResponseEntity.ok(ApiResponse.success(projectService.findById(id))); } - + @GetMapping("/code/{code}") public ResponseEntity> findByCode(@PathVariable String code) { return ResponseEntity.ok(ApiResponse.success(projectService.findByCode(code))); } - + @PostMapping public ResponseEntity> create(@RequestBody Project project) { return ResponseEntity.ok(ApiResponse.success(projectService.create(project))); } - + @PutMapping("/{id}") public ResponseEntity> update(@PathVariable UUID id, @RequestBody Project project) { return ResponseEntity.ok(ApiResponse.success(projectService.update(id, project))); } - + @DeleteMapping("/{id}") public ResponseEntity> delete(@PathVariable UUID id) { projectService.delete(id); return ResponseEntity.ok(ApiResponse.success()); } + + // ======================================== + // PM-003: 成员管理 + // ======================================== + + /** + * PM-003: 获取项目成员列表 + */ + @GetMapping("/{id}/members") + public ResponseEntity>> 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> 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> 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> getStatistics(@PathVariable("id") UUID projectId) { + return ResponseEntity.ok(ApiResponse.success(projectStatisticsService.getStatistics(projectId))); + } + + // ======================================== + // PM-006: 状态管理 + // ======================================== + + /** + * PM-006: 变更项目状态 + */ + @PutMapping("/{id}/status") + public ResponseEntity> 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> getConfig(@PathVariable("id") UUID projectId) { + return ResponseEntity.ok(ApiResponse.success(projectConfigService.getConfig(projectId))); + } + + /** + * PM-008: 更新项目配置 + */ + @PutMapping("/{id}/config") + public ResponseEntity> updateConfig( + @PathVariable("id") UUID projectId, + @RequestBody ProjectConfigDTO dto) { + return ResponseEntity.ok(ApiResponse.success(projectConfigService.updateConfig(projectId, dto))); + } } diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/controller/SpaceNodeController.java b/module-mdm/src/main/java/com/ether/pms/mdm/controller/SpaceNodeController.java index 49731aa..1ff3b4a 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/controller/SpaceNodeController.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/controller/SpaceNodeController.java @@ -1,8 +1,12 @@ 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.service.SpaceNodeService; -import com.ether.pms.common.ApiResponse; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -11,49 +15,64 @@ import java.util.List; import java.util.UUID; @RestController -@RequestMapping("/api/space-nodes") +@RequestMapping("/api/v1/mdm/space-nodes") @RequiredArgsConstructor public class SpaceNodeController { - + private final SpaceNodeService spaceNodeService; - + @GetMapping public ResponseEntity>> findAll() { return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findAll())); } - + @GetMapping("/{id}") public ResponseEntity> findById(@PathVariable UUID id) { return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findById(id))); } - - @GetMapping("/project/{projectCode}") - public ResponseEntity>> findByProject(@PathVariable String projectCode) { - return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByProject(projectCode))); + + @GetMapping("/project/{projectId}") + public ResponseEntity>> findByProject(@PathVariable UUID projectId) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByProjectId(projectId))); } - - @GetMapping("/project/{projectCode}/type/{nodeType}") + + @GetMapping("/project/{projectId}/tree") + public ResponseEntity>> getTree(@PathVariable UUID projectId) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.getTreeByProjectId(projectId))); + } + + @GetMapping("/project/{projectId}/roots") + public ResponseEntity>> getRoots(@PathVariable UUID projectId) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findRootsByProjectId(projectId))); + } + + @GetMapping("/project/{projectId}/type/{nodeType}") public ResponseEntity>> findByProjectAndType( - @PathVariable String projectCode, - @PathVariable String nodeType) { - return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByProjectAndType(projectCode, nodeType))); + @PathVariable UUID projectId, + @PathVariable SpaceNode.NodeType nodeType) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByProjectIdAndNodeType(projectId, nodeType))); } - + + @GetMapping("/parent/{parentId}/children") + public ResponseEntity>> getChildren(@PathVariable UUID parentId) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findChildren(parentId))); + } + @GetMapping("/parent/{parentCode}") public ResponseEntity>> findByParent(@PathVariable String parentCode) { - return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByParent(parentCode))); + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByParentCode(parentCode))); } - + @PostMapping - public ResponseEntity> create(@RequestBody SpaceNode spaceNode) { - return ResponseEntity.ok(ApiResponse.success(spaceNodeService.create(spaceNode))); + public ResponseEntity> create(@Valid @RequestBody SpaceNodeCreateDTO dto) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.create(dto))); } - + @PutMapping("/{id}") - public ResponseEntity> update(@PathVariable UUID id, @RequestBody SpaceNode spaceNode) { - return ResponseEntity.ok(ApiResponse.success(spaceNodeService.update(id, spaceNode))); + public ResponseEntity> update(@PathVariable UUID id, @RequestBody SpaceNodeUpdateDTO dto) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.update(id, dto))); } - + @DeleteMapping("/{id}") public ResponseEntity> delete(@PathVariable UUID id) { spaceNodeService.delete(id); diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/AddMemberRequest.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/AddMemberRequest.java new file mode 100644 index 0000000..58dd987 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/AddMemberRequest.java @@ -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 userIds; + + /** + * 项目中的角色 + */ + @NotNull(message = "项目角色不能为空") + private String roleInProject; +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/ChangeStatusRequest.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ChangeStatusRequest.java new file mode 100644 index 0000000..73669c6 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ChangeStatusRequest.java @@ -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; +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/PageResponse.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/PageResponse.java new file mode 100644 index 0000000..95c7b06 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/PageResponse.java @@ -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 { + + private List content; + + private int page; + + private int size; + + private long totalElements; + + private int totalPages; + + private boolean first; + + private boolean last; + + public static PageResponse of(List 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 + ); + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectConfigDTO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectConfigDTO.java new file mode 100644 index 0000000..72396c5 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectConfigDTO.java @@ -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; +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectMemberDTO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectMemberDTO.java new file mode 100644 index 0000000..bcb61d5 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectMemberDTO.java @@ -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; +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectQueryRequest.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectQueryRequest.java new file mode 100644 index 0000000..06c2a43 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectQueryRequest.java @@ -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"; +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectSelectorItem.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectSelectorItem.java new file mode 100644 index 0000000..c98d252 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectSelectorItem.java @@ -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; + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeCreateDTO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeCreateDTO.java new file mode 100644 index 0000000..c44f70c --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeCreateDTO.java @@ -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; +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeTreeDTO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeTreeDTO.java new file mode 100644 index 0000000..1ac8a1d --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeTreeDTO.java @@ -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 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); + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeUpdateDTO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeUpdateDTO.java new file mode 100644 index 0000000..9a8306b --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeUpdateDTO.java @@ -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; +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/Project.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/Project.java index 69f9fcc..626fd25 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/entity/Project.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/entity/Project.java @@ -36,9 +36,25 @@ public class Project { @Column(length = 100) private String address; - @Size(max = 20, message = "项目类型长度不能超过20位") - @Column(length = 20) - private String projectType; + @Column(name = "project_type", length = 20) + @Enumerated(EnumType.STRING) + 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) private String province; diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectConfig.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectConfig.java new file mode 100644 index 0000000..a4d4026 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectConfig.java @@ -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(); + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectStatistics.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectStatistics.java new file mode 100644 index 0000000..b7671f3 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectStatistics.java @@ -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(); + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectStatusHistory.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectStatusHistory.java new file mode 100644 index 0000000..f62392a --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/entity/ProjectStatusHistory.java @@ -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(); + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/SpaceNode.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/SpaceNode.java index ef49b8c..2d7529d 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/entity/SpaceNode.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/entity/SpaceNode.java @@ -5,66 +5,208 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.UUID; @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 public class SpaceNode { - + @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - + + @NotNull(message = "项目ID不能为空") + @Column(name = "project_id", nullable = false) + private UUID projectId; + @NotNull(message = "空间节点代码不能为空") @Size(min = 2, max = 50, message = "空间节点代码长度必须在2-50位之间") @Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "空间节点代码只能包含字母、数字、连字符和下划线") @Column(nullable = false, length = 50) private String code; - + @NotNull(message = "空间节点名称不能为空") @Size(min = 2, max = 100, message = "空间节点名称长度必须在2-100位之间") @Column(nullable = false, length = 100) 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 = "空间节点类型不能为空") - @Size(max = 50, message = "空间节点类型长度不能超过50位") - @Column(nullable = false, length = 50) - private String nodeType; - - @Size(max = 50, message = "父节点代码长度不能超过50位") - @Column(length = 50) + @Column(name = "node_type", nullable = false, length = 30) + @Enumerated(EnumType.STRING) + private NodeType nodeType; + + @Column(length = 30) + private String usageType; + + @Column(name = "parent_id") + private UUID parentId; + + @Column(name = "parent_code", length = 50) private String parentCode; - - @NotNull(message = "项目代码不能为空") - @Size(max = 50, message = "项目代码长度不能超过50位") - @Column(nullable = false) - private String projectCode; - - private Integer sortOrder; - - @Column(length = 50) - private String building; - - @Column(length = 50) - private String unit; - - @Column(length = 50) - private String floor; - - @Column(length = 50) - private String roomNumber; - - private Integer area; - + + @Column(name = "tree_path", length = 1000) + private String treePath; + + @Column(name = "tree_path_name", length = 1000) + private String treePathName; + + @Column(name = "level") + private Integer level = 0; + + @Column(name = "sort_order") + private Integer sortOrder = 0; + @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; - + + @Column(name = "updated_at") 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 public void prePersist() { this.createdAt = LocalDateTime.now(); @@ -75,27 +217,72 @@ public class SpaceNode { if (this.sortOrder == null) { this.sortOrder = 0; } + if (this.level == null) { + this.level = 0; + } + if (this.isDeleted == null) { + this.isDeleted = false; + } } - + @PreUpdate public void preUpdate() { this.updatedAt = LocalDateTime.now(); } - - public enum NodeType { - PROJECT("项目"), - PHASE("期"), - BUILDING("楼栋"), - UNIT("单元"), - FLOOR("楼层"), - ROOM("房间"), - PARKING("车位"), - STORE("商铺"); - + + public enum NodeCategory { + BUILDING("建筑空间"), + PARKING("停车空间"), + FACILITY("设施空间"), + AREA("区域空间"); + private final String desc; - - NodeType(String desc) { + + NodeCategory(String 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; + } } } diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/enums/ProjectStatus.java b/module-mdm/src/main/java/com/ether/pms/mdm/enums/ProjectStatus.java new file mode 100644 index 0000000..8aa3357 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/enums/ProjectStatus.java @@ -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; + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectConfigRepository.java b/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectConfigRepository.java new file mode 100644 index 0000000..d7cd7af --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectConfigRepository.java @@ -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 { + + Optional findByProjectId(UUID projectId); + + void deleteByProjectId(UUID projectId); + + boolean existsByProjectId(UUID projectId); +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectRepository.java b/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectRepository.java index 836a344..5c53c41 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectRepository.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectRepository.java @@ -1,15 +1,40 @@ package com.ether.pms.mdm.repository; 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.JpaSpecificationExecutor; +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 ProjectRepository extends JpaRepository { - +public interface ProjectRepository extends JpaRepository, JpaSpecificationExecutor { + Optional findByCode(String code); - + boolean existsByCode(String code); + + Page 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 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 findActiveProjectsForSelector(); + + @Query("SELECT COUNT(p) FROM Project p WHERE p.status = :status") + long countByStatus(@Param("status") String status); + + boolean existsByCodeAndIdNot(String code, UUID id); } diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectStatisticsRepository.java b/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectStatisticsRepository.java new file mode 100644 index 0000000..23dbc16 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectStatisticsRepository.java @@ -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 { + + Optional findByProjectId(UUID projectId); + + void deleteByProjectId(UUID projectId); + + boolean existsByProjectId(UUID projectId); +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectStatusHistoryRepository.java b/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectStatusHistoryRepository.java new file mode 100644 index 0000000..65999df --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/repository/ProjectStatusHistoryRepository.java @@ -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 { + + List findByProjectIdOrderByCreatedAtDesc(UUID projectId); + + Page findByProjectIdOrderByCreatedAtDesc(UUID projectId, Pageable pageable); + + @Query("SELECT h FROM ProjectStatusHistory h WHERE h.projectId = :projectId ORDER BY h.createdAt DESC LIMIT 1") + Optional findLatestByProjectId(@Param("projectId") UUID projectId); + + long countByProjectId(UUID projectId); +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/SpaceNodeRepository.java b/module-mdm/src/main/java/com/ether/pms/mdm/repository/SpaceNodeRepository.java index ec80642..50b6501 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/repository/SpaceNodeRepository.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/repository/SpaceNodeRepository.java @@ -5,16 +5,25 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; import java.util.UUID; @Repository public interface SpaceNodeRepository extends JpaRepository { - - List findByProjectCode(String projectCode); - - List findByProjectCodeAndNodeType(String projectCode, String nodeType); - - List findByParentCode(String parentCode); - - boolean existsByCode(String code); + + boolean existsByProjectIdAndCode(UUID projectId, String code); + + List findByIsDeletedFalse(); + + Optional findByIdAndIsDeletedFalse(UUID id); + + List findByProjectIdAndIsDeletedFalseOrderBySortOrderAsc(UUID projectId); + + List findByProjectIdAndNodeTypeAndIsDeletedFalse(UUID projectId, SpaceNode.NodeType nodeType); + + List findByProjectIdAndParentIdIsNullAndIsDeletedFalse(UUID projectId); + + List findByParentIdAndIsDeletedFalseOrderBySortOrderAsc(UUID parentId); + + List findByParentCodeAndIsDeletedFalse(String parentCode); } diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectConfigService.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectConfigService.java new file mode 100644 index 0000000..0d9f928 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectConfigService.java @@ -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; + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectMemberService.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectMemberService.java new file mode 100644 index 0000000..ea61fce --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectMemberService.java @@ -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 getMembers(UUID projectId, Pageable pageable) { + // 验证项目存在 + if (!projectRepository.existsById(projectId)) { + throw new BusinessException(ErrorCode.PROJECT_002); + } + + // 分页查询用户项目关联 + Page userProjectPage = userProjectRepository.findByProjectId(projectId, pageable); + + // 获取用户ID列表 + List userIds = userProjectPage.getContent().stream() + .map(UserProject::getUserId) + .collect(Collectors.toList()); + + // 批量查询用户信息 + Map userMap; + if (!userIds.isEmpty()) { + List users = userRepository.findAllById(userIds); + userMap = users.stream() + .collect(Collectors.toMap(User::getId, Function.identity())); + } else { + userMap = Collections.emptyMap(); + } + + // 使用 final 变量在 lambda 中 + final Map finalUserMap = userMap; + + // 构建DTO + List 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 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); + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectService.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectService.java index 1054dc1..8282c18 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectService.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectService.java @@ -1,36 +1,112 @@ 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.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.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Year; import java.util.List; import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class ProjectService { - + private final ProjectRepository projectRepository; - + public List findAll() { return projectRepository.findAll(); } - + public Project findById(UUID id) { return projectRepository.findById(id) .orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_002)); } - + public Project findByCode(String code) { return projectRepository.findByCode(code) .orElseThrow(() -> new BusinessException(ErrorCode.PROJECT_002)); } - + + /** + * PM-001: 分页查询项目 + */ + public PageResponse 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 projectPage = projectRepository.searchProjects( + request.getKeyword(), + request.getStatus(), + pageable + ); + + return PageResponse.of( + projectPage.getContent(), + projectPage.getNumber(), + projectPage.getSize(), + projectPage.getTotalElements() + ); + } + + /** + * PM-010: 获取项目选择器列表 + */ + public List getSelectorList() { + List 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 public Project create(Project project) { if (projectRepository.existsByCode(project.getCode())) { @@ -38,11 +114,19 @@ public class ProjectService { } return projectRepository.save(project); } - + @Transactional public Project update(UUID id, Project project) { 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) { existing.setName(project.getName()); } @@ -94,12 +178,58 @@ public class ProjectService { if (project.getContactPhone() != null) { existing.setContactPhone(project.getContactPhone()); } - + return projectRepository.save(existing); } - + @Transactional public void delete(UUID 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; + }; + } } diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectStatisticsService.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectStatisticsService.java new file mode 100644 index 0000000..f75a8f7 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/service/ProjectStatisticsService.java @@ -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); + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/SpaceNodeService.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/SpaceNodeService.java index 9bfeade..90a65c8 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/service/SpaceNodeService.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/service/SpaceNodeService.java @@ -1,91 +1,225 @@ 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.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 org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class SpaceNodeService { - + private final SpaceNodeRepository spaceNodeRepository; - + public List findAll() { - return spaceNodeRepository.findAll(); + return spaceNodeRepository.findByIsDeletedFalse(); } - + public SpaceNode findById(UUID id) { - return spaceNodeRepository.findById(id) - .orElseThrow(() -> new BusinessException(ErrorCode.SPACE_002)); + return spaceNodeRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new BusinessException(ErrorCode.SPACE_002)); } - - public List findByProject(String projectCode) { - return spaceNodeRepository.findByProjectCode(projectCode); + + public List findByProjectId(UUID projectId) { + return spaceNodeRepository.findByProjectIdAndIsDeletedFalseOrderBySortOrderAsc(projectId); } - - public List findByProjectAndType(String projectCode, String nodeType) { - return spaceNodeRepository.findByProjectCodeAndNodeType(projectCode, nodeType); + + public List findByProjectIdAndNodeType(UUID projectId, SpaceNode.NodeType nodeType) { + return spaceNodeRepository.findByProjectIdAndNodeTypeAndIsDeletedFalse(projectId, nodeType); } - - public List findByParent(String parentCode) { - return spaceNodeRepository.findByParentCode(parentCode); + + public List findRootsByProjectId(UUID projectId) { + return spaceNodeRepository.findByProjectIdAndParentIdIsNullAndIsDeletedFalse(projectId); } - + + public List findChildren(UUID parentId) { + return spaceNodeRepository.findByParentIdAndIsDeletedFalseOrderBySortOrderAsc(parentId); + } + + public List findByParentCode(String parentCode) { + return spaceNodeRepository.findByParentCodeAndIsDeletedFalse(parentCode); + } + + public List getTreeByProjectId(UUID projectId) { + List allNodes = findByProjectId(projectId); + return buildTree(allNodes); + } + + public List buildTree(List nodes) { + if (nodes == null || nodes.isEmpty()) { + return new ArrayList<>(); + } + + List rootNodes = new ArrayList<>(); + List 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 - public SpaceNode create(SpaceNode spaceNode) { - if (spaceNodeRepository.existsByCode(spaceNode.getCode())) { + public SpaceNode create(SpaceNodeCreateDTO dto) { + if (spaceNodeRepository.existsByProjectIdAndCode(dto.getProjectId(), dto.getCode())) { 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 - public SpaceNode update(UUID id, SpaceNode spaceNode) { + public SpaceNode update(UUID id, SpaceNodeUpdateDTO dto) { SpaceNode existing = findById(id); - - if (spaceNode.getName() != null) { - existing.setName(spaceNode.getName()); + + if (dto.getName() != null) { + existing.setName(dto.getName()); } - if (spaceNode.getNodeType() != null) { - existing.setNodeType(spaceNode.getNodeType()); + if (dto.getFullName() != null) { + existing.setFullName(dto.getFullName()); } - if (spaceNode.getParentCode() != null) { - existing.setParentCode(spaceNode.getParentCode()); + if (dto.getShortName() != null) { + existing.setShortName(dto.getShortName()); } - if (spaceNode.getSortOrder() != null) { - existing.setSortOrder(spaceNode.getSortOrder()); + if (dto.getNodeCategory() != null) { + existing.setNodeCategory(dto.getNodeCategory()); } - if (spaceNode.getBuilding() != null) { - existing.setBuilding(spaceNode.getBuilding()); + if (dto.getNodeType() != null) { + existing.setNodeType(dto.getNodeType()); } - if (spaceNode.getUnit() != null) { - existing.setUnit(spaceNode.getUnit()); + if (dto.getUsageType() != null) { + existing.setUsageType(dto.getUsageType()); } - if (spaceNode.getFloor() != null) { - existing.setFloor(spaceNode.getFloor()); + if (dto.getSortOrder() != null) { + existing.setSortOrder(dto.getSortOrder()); } - if (spaceNode.getRoomNumber() != null) { - existing.setRoomNumber(spaceNode.getRoomNumber()); + if (dto.getStatus() != null) { + existing.setStatus(dto.getStatus()); } - if (spaceNode.getArea() != null) { - existing.setArea(spaceNode.getArea()); + if (dto.getDeliveryStatus() != null) { + existing.setDeliveryStatus(dto.getDeliveryStatus()); } - if (spaceNode.getStatus() != null) { - existing.setStatus(spaceNode.getStatus()); + if (dto.getDecorationStatus() != null) { + 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); } - + @Transactional public void delete(UUID id) { - spaceNodeRepository.deleteById(id); + SpaceNode node = findById(id); + List children = findChildren(id); + if (!children.isEmpty()) { + throw new BusinessException(ErrorCode.SPACE_003); + } + node.setIsDeleted(true); + spaceNodeRepository.save(node); } } diff --git a/module-mdm/src/test/java/com/ether/pms/mdm/entity/SpaceNodeTest.java b/module-mdm/src/test/java/com/ether/pms/mdm/entity/SpaceNodeTest.java new file mode 100644 index 0000000..5195b63 --- /dev/null +++ b/module-mdm/src/test/java/com/ether/pms/mdm/entity/SpaceNodeTest.java @@ -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()); + } +} diff --git a/module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectMemberServiceTest.java b/module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectMemberServiceTest.java new file mode 100644 index 0000000..e6d7c8c --- /dev/null +++ b/module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectMemberServiceTest.java @@ -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 userProjects = new ArrayList<>(); + userProjects.add(testUserProject); + Page 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 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()); + } +} diff --git a/module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectServiceTest.java b/module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectServiceTest.java new file mode 100644 index 0000000..c75e93c --- /dev/null +++ b/module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectServiceTest.java @@ -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 projects = new ArrayList<>(); + projects.add(testProject); + Page projectPage = new PageImpl<>(projects, PageRequest.of(0, 20), 1); + + when(projectRepository.searchProjects(eq("测试"), eq(null), any(Pageable.class))) + .thenReturn(projectPage); + + // When + PageResponse 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 projects = new ArrayList<>(); + projects.add(testProject); + Page projectPage = new PageImpl<>(projects, PageRequest.of(0, 20), 1); + + when(projectRepository.searchProjects(eq(null), eq("ACTIVE"), any(Pageable.class))) + .thenReturn(projectPage); + + // When + PageResponse 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 projects = new ArrayList<>(); + Page projectPage = new PageImpl<>(projects, PageRequest.of(1, 10), 25); + + when(projectRepository.searchProjects(eq(null), eq(null), any(Pageable.class))) + .thenReturn(projectPage); + + // When + PageResponse 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 emptyPage = new PageImpl<>(new ArrayList<>(), PageRequest.of(0, 20), 0); + when(projectRepository.searchProjects(eq("不存在"), eq(null), any(Pageable.class))) + .thenReturn(emptyPage); + + // When + PageResponse result = projectService.queryProjects(request); + + // Then + assertNotNull(result); + assertTrue(result.getContent().isEmpty()); + assertEquals(0, result.getTotalElements()); + } + + // ======================================== + // PM-010: 选择器列表测试 + // ======================================== + + @Test + void testGetSelectorList_shouldReturnActiveProjects() { + // Given + List 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); + } +} diff --git a/module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectStatisticsServiceTest.java b/module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectStatisticsServiceTest.java new file mode 100644 index 0000000..28a3d47 --- /dev/null +++ b/module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectStatisticsServiceTest.java @@ -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 + )); + } +} diff --git a/sql/V2.1__project_enhancements.sql b/sql/V2.1__project_enhancements.sql new file mode 100644 index 0000000..7a0917f --- /dev/null +++ b/sql/V2.1__project_enhancements.sql @@ -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; diff --git a/sql/audit_log.sql b/sql/audit_log.sql new file mode 100644 index 0000000..d545ac6 --- /dev/null +++ b/sql/audit_log.sql @@ -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); diff --git a/sql/init.sql b/sql/init.sql index 1bfcce2..17ec85d 100644 --- a/sql/init.sql +++ b/sql/init.sql @@ -1,5 +1,6 @@ -- Ether PMS Database Initialization Script -- Database: ether_pms +-- Version: 2.0 -- ============================================ -- Auth Module Tables @@ -23,9 +24,9 @@ CREATE TABLE IF NOT EXISTS auth_user ( created_by UUID ); -CREATE INDEX idx_auth_user_username ON auth_user(username); -CREATE INDEX 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_username ON auth_user(username); +CREATE INDEX IF NOT EXISTS idx_auth_user_phone ON auth_user(phone); +CREATE INDEX IF NOT EXISTS idx_auth_user_status ON auth_user(status); -- Role Table CREATE TABLE IF NOT EXISTS auth_role ( @@ -41,8 +42,8 @@ CREATE TABLE IF NOT EXISTS auth_role ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX 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_code ON auth_role(code); +CREATE INDEX IF NOT EXISTS idx_auth_role_project ON auth_role(project_id); -- Permission Table CREATE TABLE IF NOT EXISTS auth_permission ( @@ -59,9 +60,9 @@ CREATE TABLE IF NOT EXISTS auth_permission ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX idx_auth_permission_code ON auth_permission(code); -CREATE INDEX 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_code ON auth_permission(code); +CREATE INDEX IF NOT EXISTS idx_auth_permission_type ON auth_permission(type); +CREATE INDEX IF NOT EXISTS idx_auth_permission_parent ON auth_permission(parent_code); -- User-Role Relation Table 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 ); -CREATE INDEX 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_code ON mdm_project(code); +CREATE INDEX IF NOT EXISTS idx_mdm_project_status ON mdm_project(status); -- Space Node Table CREATE TABLE IF NOT EXISTS mdm_space_node ( @@ -129,46 +130,99 @@ CREATE TABLE IF NOT EXISTS mdm_space_node ( UNIQUE(code, project_code) ); -CREATE INDEX 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 idx_mdm_space_node_parent ON mdm_space_node(parent_code); +CREATE INDEX IF NOT EXISTS idx_mdm_space_node_project ON mdm_space_node(project_code); +CREATE INDEX IF NOT EXISTS idx_mdm_space_node_type ON mdm_space_node(node_type); +CREATE INDEX IF NOT EXISTS idx_mdm_space_node_parent ON mdm_space_node(parent_code); -- ============================================ -- Initial Data -- ============================================ --- Insert default admin user +-- Insert default admin user -- Password: Admin123! (BCrypt encrypted) -- Password requirements: 8-20 chars, uppercase, lowercase, digit, special char 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 INTO auth_role (code, name, description, type, data_scope, status) -VALUES +VALUES ('SYSTEM_ADMIN', '系统管理员', '系统超级管理员', 'SYSTEM', 'ALL', 'ENABLED'), ('PROJECT_ADMIN', '项目管理员', '项目管理员', 'PROJECT', 'PROJECT', 'ENABLED'), - ('EMPLOYEE', '普通员工', '普通员工', 'DEPARTMENT', 'SELF', 'ENABLED'); + ('EMPLOYEE', '普通员工', '普通员工', 'DEPARTMENT', 'SELF', 'ENABLED') +ON CONFLICT (code) DO NOTHING; --- Insert default permissions -INSERT INTO auth_permission (code, name, type, resource, method, sort_order) -VALUES - ('dashboard', '仪表盘', 'MENU', '/dashboard', 'GET', 1), - ('user:list', '用户列表', 'BUTTON', '/api/users', 'GET', 10), - ('user:create', '创建用户', 'BUTTON', '/api/users', 'POST', 11), - ('user:update', '更新用户', 'BUTTON', '/api/users', 'PUT', 12), - ('user:delete', '删除用户', 'BUTTON', '/api/users', 'DELETE', 13), - ('role:list', '角色列表', 'BUTTON', '/api/roles', 'GET', 20), - ('role:create', '创建角色', 'BUTTON', '/api/roles', 'POST', 21), - ('role:update', '更新角色', 'BUTTON', '/api/roles', 'PUT', 22), - ('role:delete', '删除角色', 'BUTTON', '/api/roles', 'DELETE', 23), - ('project:list', '项目列表', 'BUTTON', '/api/projects', 'GET', 30), - ('project:create', '创建项目', 'BUTTON', '/api/projects', 'POST', 31), - ('project:update', '更新项目', 'BUTTON', '/api/projects', 'PUT', 32), - ('project:delete', '删除项目', 'BUTTON', '/api/projects', 'DELETE', 33); +-- Insert comprehensive permissions +-- Format: module:action (e.g., dashboard:view, system:menu) +INSERT INTO auth_permission (code, name, type, resource, method, description, sort_order) +VALUES + -- Dashboard / 仪表盘 + ('dashboard:view', '查看仪表盘', 'MENU', '/dashboard', 'GET', '查看仪表盘', 1), + + -- System Management / 系统管理 + ('system:menu', '系统管理', 'MENU', '/system', 'GET', '系统管理菜单', 99), + ('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:user:resetPwd', '重置密码', 'BUTTON', '/api/users/*/reset-password', 'POST', '重置用户密码', 104), + ('system:user:export', '导出用户', 'BUTTON', '/api/users/export', 'GET', '导出用户数据', 105), + + -- 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 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'; +SELECT r.id, p.id +FROM auth_role r, auth_permission p +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 +); diff --git a/sql/reset_auth.sql b/sql/reset_auth.sql new file mode 100644 index 0000000..5afd08e --- /dev/null +++ b/sql/reset_auth.sql @@ -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;