diff --git a/module-asset/pom.xml b/module-asset/pom.xml index 66968eb..302db17 100644 --- a/module-asset/pom.xml +++ b/module-asset/pom.xml @@ -23,6 +23,12 @@ ${project.version} + + com.ether + module-mdm + ${project.version} + + org.springframework.boot spring-boot-starter-web @@ -53,5 +59,23 @@ org.mapstruct mapstruct + + + org.apache.poi + poi-ooxml + 5.2.5 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + test + diff --git a/module-asset/src/main/java/com/ether/pms/asset/controller/EquipmentController.java b/module-asset/src/main/java/com/ether/pms/asset/controller/EquipmentController.java new file mode 100644 index 0000000..7c0f6e4 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/controller/EquipmentController.java @@ -0,0 +1,173 @@ +package com.ether.pms.asset.controller; + +import com.ether.pms.asset.entity.Equipment; +import com.ether.pms.asset.entity.EquipmentElevator; +import com.ether.pms.asset.entity.EquipmentEnergy; +import com.ether.pms.asset.entity.EquipmentFire; +import com.ether.pms.asset.entity.EquipmentHvac; +import com.ether.pms.asset.enums.EquipmentType; +import com.ether.pms.asset.enums.OwnershipType; +import com.ether.pms.asset.service.*; +import com.ether.pms.common.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping("/api/asset/equipment") +@RequiredArgsConstructor +public class EquipmentController { + + private final EquipmentService equipmentService; + private final EquipmentElevatorService elevatorService; + private final EquipmentHvacService hvacService; + private final EquipmentEnergyService energyService; + private final EquipmentFireService fireService; + + // ==================== 设备主表 CRUD ==================== + + @PostMapping + public ApiResponse createEquipment(@RequestBody Equipment equipment) { + return ApiResponse.success(equipmentService.createEquipment(equipment)); + } + + @GetMapping("/{id}") + public ApiResponse getEquipment(@PathVariable UUID id) { + return ApiResponse.success(equipmentService.getEquipmentById(id)); + } + + @PutMapping("/{id}") + public ApiResponse updateEquipment(@PathVariable UUID id, @RequestBody Equipment equipment) { + return ApiResponse.success(equipmentService.updateEquipment(id, equipment)); + } + + @DeleteMapping("/{id}") + public ApiResponse deleteEquipment(@PathVariable UUID id) { + equipmentService.deleteEquipment(id); + return ApiResponse.success(null); + } + + @PostMapping("/batch-delete") + public ApiResponse deleteEquipmentBatch(@RequestBody List ids) { + equipmentService.deleteEquipmentBatch(ids); + return ApiResponse.success(null); + } + + @PostMapping("/import") + public ApiResponse> importEquipment(@RequestParam("file") MultipartFile file, @RequestParam UUID projectId) { + return ApiResponse.success(equipmentService.importFromExcel(file, projectId)); + } + + @GetMapping("/export") + public ResponseEntity exportEquipment(@RequestParam UUID projectId) { + byte[] data = equipmentService.exportToExcel(projectId); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + headers.setContentDispositionFormData("attachment", "设备列表.xlsx"); + return ResponseEntity.ok().headers(headers).body(data); + } + + @GetMapping("/by-project/{projectId}") + public ApiResponse> getEquipmentsByProject(@PathVariable UUID projectId) { + return ApiResponse.success(equipmentService.getEquipmentsByProject(projectId)); + } + + @GetMapping("/by-space/{spaceNodeId}") + public ApiResponse> getEquipmentsBySpace(@PathVariable UUID spaceNodeId) { + return ApiResponse.success(equipmentService.getEquipmentsBySpaceNode(spaceNodeId)); + } + + @GetMapping("/by-type") + public ApiResponse> getEquipmentsByType(@RequestParam EquipmentType type) { + return ApiResponse.success(equipmentService.getEquipmentsByType(type)); + } + + @GetMapping("/by-ownership") + public ApiResponse> getEquipmentsByOwnership(@RequestParam OwnershipType ownership) { + return ApiResponse.success(equipmentService.getEquipmentsByOwnership(ownership)); + } + + // ==================== 设备统计 ==================== + + @GetMapping("/stats/by-type/{projectId}") + public ApiResponse> getStatsByType(@PathVariable UUID projectId) { + return ApiResponse.success(equipmentService.getEquipmentStatsByType(projectId)); + } + + @GetMapping("/stats/by-ownership/{projectId}") + public ApiResponse> getStatsByOwnership(@PathVariable UUID projectId) { + return ApiResponse.success(equipmentService.getEquipmentStatsByOwnership(projectId)); + } + + @GetMapping("/stats/count/{projectId}") + public ApiResponse countByProject(@PathVariable UUID projectId) { + return ApiResponse.success(equipmentService.countByProject(projectId)); + } + + // ==================== 电梯扩展 ==================== + + @GetMapping("/{id}/elevator") + public ApiResponse getElevator(@PathVariable UUID id) { + return ApiResponse.success( + elevatorService.getByEquipmentId(id).orElse(null) + ); + } + + @PutMapping("/{id}/elevator") + public ApiResponse updateElevator(@PathVariable UUID id, @RequestBody EquipmentElevator elevator) { + elevator.setEquipmentId(id); + return ApiResponse.success(elevatorService.saveOrUpdate(elevator)); + } + + // ==================== 暖通扩展 ==================== + + @GetMapping("/{id}/hvac") + public ApiResponse getHvac(@PathVariable UUID id) { + return ApiResponse.success( + hvacService.getByEquipmentId(id).orElse(null) + ); + } + + @PutMapping("/{id}/hvac") + public ApiResponse updateHvac(@PathVariable UUID id, @RequestBody EquipmentHvac hvac) { + hvac.setEquipmentId(id); + return ApiResponse.success(hvacService.saveOrUpdate(hvac)); + } + + // ==================== 能源计量扩展 ==================== + + @GetMapping("/{id}/energy") + public ApiResponse getEnergy(@PathVariable UUID id) { + return ApiResponse.success( + energyService.getByEquipmentId(id).orElse(null) + ); + } + + @PutMapping("/{id}/energy") + public ApiResponse updateEnergy(@PathVariable UUID id, @RequestBody EquipmentEnergy energy) { + energy.setEquipmentId(id); + return ApiResponse.success(energyService.saveOrUpdate(energy)); + } + + // ==================== 消防扩展 ==================== + + @GetMapping("/{id}/fire") + public ApiResponse getFire(@PathVariable UUID id) { + return ApiResponse.success( + fireService.getByEquipmentId(id).orElse(null) + ); + } + + @PutMapping("/{id}/fire") + public ApiResponse updateFire(@PathVariable UUID id, @RequestBody EquipmentFire fire) { + fire.setEquipmentId(id); + return ApiResponse.success(fireService.saveOrUpdate(fire)); + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/controller/EquipmentHealthController.java b/module-asset/src/main/java/com/ether/pms/asset/controller/EquipmentHealthController.java similarity index 74% rename from module-mdm/src/main/java/com/ether/pms/mdm/controller/EquipmentHealthController.java rename to module-asset/src/main/java/com/ether/pms/asset/controller/EquipmentHealthController.java index 81c75cb..c83c632 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/controller/EquipmentHealthController.java +++ b/module-asset/src/main/java/com/ether/pms/asset/controller/EquipmentHealthController.java @@ -1,9 +1,9 @@ -package com.ether.pms.mdm.controller; +package com.ether.pms.asset.controller; +import com.ether.pms.asset.entity.EquipmentFailureHistory; +import com.ether.pms.asset.entity.EquipmentHealthScore; +import com.ether.pms.asset.service.EquipmentHealthService; import com.ether.pms.common.ApiResponse; -import com.ether.pms.mdm.entity.EquipmentFailureHistory; -import com.ether.pms.mdm.entity.EquipmentHealthScore; -import com.ether.pms.mdm.service.EquipmentHealthService; import jakarta.validation.Valid; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -14,56 +14,38 @@ import java.util.List; import java.util.UUID; @RestController -@RequestMapping("/api/v1/ops") +@RequestMapping("/api/asset/equipment-health") @RequiredArgsConstructor public class EquipmentHealthController { private final EquipmentHealthService equipmentHealthService; - /** - * 获取设备健康度 - */ - @GetMapping("/equipment-health/{equipmentId}") + @GetMapping("/{equipmentId}") public ApiResponse getEquipmentHealth(@PathVariable UUID equipmentId) { return ApiResponse.success(equipmentHealthService.getLatestHealthScore(equipmentId)); } - /** - * 获取设备健康度历史 - */ - @GetMapping("/equipment-health/{equipmentId}/history") + @GetMapping("/{equipmentId}/history") public ApiResponse> getHealthHistory(@PathVariable UUID equipmentId) { return ApiResponse.success(equipmentHealthService.getHealthHistory(equipmentId)); } - /** - * 计算设备健康度 - */ - @PostMapping("/equipment-health/calculate") + @PostMapping("/calculate") public ApiResponse calculateHealthScore(@RequestBody CalculateHealthRequest request) { return ApiResponse.success(equipmentHealthService.calculateHealthScore(request.getEquipmentId())); } - /** - * 记录故障 - */ - @PostMapping("/equipment-failure-history") + @PostMapping("/failure-history") public ApiResponse recordFailure(@Valid @RequestBody EquipmentFailureHistory failure) { return ApiResponse.success(equipmentHealthService.recordFailure(failure)); } - /** - * 获取设备故障历史 - */ - @GetMapping("/equipment-failure-history/{equipmentId}") + @GetMapping("/failure-history/{equipmentId}") public ApiResponse> getFailureHistory(@PathVariable UUID equipmentId) { return ApiResponse.success(equipmentHealthService.getFailureHistory(equipmentId)); } - /** - * 获取设备MTBF(平均故障间隔时间) - */ - @GetMapping("/equipment-mtbf/{equipmentId}") + @GetMapping("/mtbf/{equipmentId}") public ApiResponse getEquipmentMTBF( @PathVariable UUID equipmentId, @RequestParam(required = false, defaultValue = "30") Integer days) { @@ -75,10 +57,7 @@ public class EquipmentHealthController { return ApiResponse.success(response); } - /** - * 获取设备MTTR(平均修复时间) - */ - @GetMapping("/equipment-mttr/{equipmentId}") + @GetMapping("/mttr/{equipmentId}") public ApiResponse getEquipmentMTTR( @PathVariable UUID equipmentId, @RequestParam(required = false, defaultValue = "30") Integer days) { @@ -108,4 +87,4 @@ public class EquipmentHealthController { private Integer days; private BigDecimal mttrHours; } -} \ No newline at end of file +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/controller/OwnershipEntityController.java b/module-asset/src/main/java/com/ether/pms/asset/controller/OwnershipEntityController.java new file mode 100644 index 0000000..6cb954d --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/controller/OwnershipEntityController.java @@ -0,0 +1,67 @@ +package com.ether.pms.asset.controller; + +import com.ether.pms.asset.entity.OwnershipEntity; +import com.ether.pms.asset.repository.OwnershipEntityRepository; +import com.ether.pms.common.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/asset/ownership-entity") +@RequiredArgsConstructor +public class OwnershipEntityController { + + private final OwnershipEntityRepository ownershipEntityRepository; + + @PostMapping + public ApiResponse create(@RequestBody OwnershipEntity entity) { + return ApiResponse.success(ownershipEntityRepository.save(entity)); + } + + @GetMapping("/{id}") + public ApiResponse getById(@PathVariable UUID id) { + return ApiResponse.success( + ownershipEntityRepository.findByIdAndIsDeletedFalse(id).orElse(null) + ); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable UUID id, @RequestBody OwnershipEntity entity) { + OwnershipEntity existing = ownershipEntityRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new RuntimeException("归属主体不存在: " + id)); + existing.setEntityName(entity.getEntityName()); + existing.setContactPerson(entity.getContactPerson()); + existing.setContactPhone(entity.getContactPhone()); + existing.setContactAddress(entity.getContactAddress()); + existing.setBusinessLicense(entity.getBusinessLicense()); + existing.setLegalRepresentative(entity.getLegalRepresentative()); + existing.setContractNo(entity.getContractNo()); + existing.setContractStartDate(entity.getContractStartDate()); + existing.setContractEndDate(entity.getContractEndDate()); + existing.setRentalFee(entity.getRentalFee()); + existing.setStatus(entity.getStatus()); + return ApiResponse.success(ownershipEntityRepository.save(existing)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable UUID id) { + OwnershipEntity entity = ownershipEntityRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new RuntimeException("归属主体不存在: " + id)); + entity.setIsDeleted(true); + ownershipEntityRepository.save(entity); + return ApiResponse.success(null); + } + + @GetMapping("/by-type") + public ApiResponse> getByType(@RequestParam OwnershipEntity.EntityType type) { + return ApiResponse.success(ownershipEntityRepository.findByEntityTypeAndIsDeletedFalse(type)); + } + + @GetMapping + public ApiResponse> getAll() { + return ApiResponse.success(ownershipEntityRepository.findByIsDeletedFalse()); + } +} \ No newline at end of file diff --git a/module-asset/src/main/java/com/ether/pms/asset/dto/EquipmentCreateDTO.java b/module-asset/src/main/java/com/ether/pms/asset/dto/EquipmentCreateDTO.java new file mode 100644 index 0000000..6455b15 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/dto/EquipmentCreateDTO.java @@ -0,0 +1,60 @@ +package com.ether.pms.asset.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.UUID; + +@Data +public class EquipmentCreateDTO { + + @NotBlank(message = "设备名称不能为空") + private String name; + + @NotNull(message = "项目ID不能为空") + private UUID projectId; + + private UUID spaceNodeId; + + private Boolean isEquipment = true; + + private Integer designLifeYears; + + private BigDecimal ratedPower; + + private BigDecimal ratedVoltage; + + private BigDecimal ratedCurrent; + + private String maintenanceVendor; + + private String maintenanceVendorContact; + + private String maintenanceVendorPhone; + + private String maintenanceContractNo; + + private LocalDate maintenanceContractStart; + + private LocalDate maintenanceContractEnd; + + private String specialEquipmentType; + + private String specialEquipmentCert; + + private Integer inspectionCycle; + + private LocalDate nextInspectionDate; + + private LocalDate lastInspectionDate; + + private String lastInspectionResult; + + private BigDecimal energyConsumptionStandard; + + private String installationEnvironment; + + private String protectionLevel; +} \ No newline at end of file diff --git a/module-asset/src/main/java/com/ether/pms/asset/dto/SpaceNodeDTO.java b/module-asset/src/main/java/com/ether/pms/asset/dto/SpaceNodeDTO.java new file mode 100644 index 0000000..708a516 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/dto/SpaceNodeDTO.java @@ -0,0 +1,18 @@ +package com.ether.pms.asset.dto; + +import lombok.Data; + +import java.util.UUID; + +@Data +public class SpaceNodeDTO { + private UUID id; + private String nodeName; + private String nodeCode; + private String nodeType; + private UUID parentId; + private Integer floor; + private String building; + private String unit; + private String roomNo; +} \ No newline at end of file diff --git a/module-asset/src/main/java/com/ether/pms/asset/dto/SpaceNodeEquipmentDTO.java b/module-asset/src/main/java/com/ether/pms/asset/dto/SpaceNodeEquipmentDTO.java new file mode 100644 index 0000000..c34e481 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/dto/SpaceNodeEquipmentDTO.java @@ -0,0 +1,41 @@ +package com.ether.pms.asset.dto; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class SpaceNodeEquipmentDTO extends SpaceNodeDTO { + + private Boolean isEquipment; + private Integer designLifeYears; + private BigDecimal ratedPower; + private BigDecimal ratedVoltage; + private BigDecimal ratedCurrent; + private String maintenanceVendor; + private String maintenanceVendorContact; + private String maintenanceVendorPhone; + private String maintenanceContractNo; + private LocalDate maintenanceContractStart; + private LocalDate maintenanceContractEnd; + private String specialEquipmentType; + private String specialEquipmentCert; + private Integer inspectionCycle; + private LocalDate nextInspectionDate; + private LocalDate lastInspectionDate; + private String lastInspectionResult; + private List commonSpareParts; + private BigDecimal energyConsumptionStandard; + private String installationEnvironment; + private String protectionLevel; + + @Data + public static class SparePartInfo { + private String name; + private String model; + private Integer quantity; + } +} \ No newline at end of file diff --git a/module-asset/src/main/java/com/ether/pms/asset/entity/Equipment.java b/module-asset/src/main/java/com/ether/pms/asset/entity/Equipment.java new file mode 100644 index 0000000..aa86378 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/entity/Equipment.java @@ -0,0 +1,224 @@ +package com.ether.pms.asset.entity; + +import com.ether.pms.asset.enums.EquipmentStatus; +import com.ether.pms.asset.enums.EquipmentType; +import com.ether.pms.asset.enums.OwnershipType; +import com.ether.pms.asset.enums.SystemType; +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Entity +@Table(name = "asset_equipment", indexes = { + @Index(name = "idx_equipment_project", columnList = "project_id"), + @Index(name = "idx_equipment_space", columnList = "space_node_id"), + @Index(name = "idx_equipment_type", columnList = "equipment_type"), + @Index(name = "idx_equipment_ownership", columnList = "ownership_type"), + @Index(name = "idx_equipment_code", columnList = "equipment_code"), + @Index(name = "idx_equipment_status", columnList = "status") +}) +@Data +public class Equipment { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "project_id") + private UUID projectId; + + @Column(name = "space_node_id") + private UUID spaceNodeId; + + @Column(name = "equipment_code", unique = true, nullable = false, length = 50) + private String equipmentCode; + + @Column(name = "equipment_name", nullable = false, length = 100) + private String equipmentName; + + @Enumerated(EnumType.STRING) + @Column(name = "equipment_type", nullable = false, length = 30) + private EquipmentType equipmentType; + + @Column(name = "equipment_category", length = 50) + private String equipmentCategory; + + @Enumerated(EnumType.STRING) + @Column(name = "system_type", length = 50) + private SystemType systemType; + + @Enumerated(EnumType.STRING) + @Column(name = "ownership_type", nullable = false, length = 20) + private OwnershipType ownershipType = OwnershipType.PROJECT; + + @Column(name = "owning_entity_id") + private UUID owningEntityId; + + @Column(name = "owning_entity_name", length = 100) + private String owningEntityName; + + @Column(name = "asset_code", length = 50) + private String assetCode; + + @Column(name = "serial_number", length = 100) + private String serialNumber; + + @Column(length = 100) + private String model; + + @Column(length = 100) + private String manufacturer; + + @Column(length = 100) + private String supplier; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + private EquipmentStatus status = EquipmentStatus.ACTIVE; + + @Column(name = "operation_status", length = 20) + private String operationStatus; + + @Column(name = "installation_location", length = 200) + private String installationLocation; + + @Column(name = "installation_date") + private LocalDate installationDate; + + @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 = "purchase_date") + private LocalDate purchaseDate; + + @Column(name = "purchase_price", precision = 12, scale = 2) + private BigDecimal purchasePrice; + + @Column(name = "warranty_expire_date") + private LocalDate warrantyExpireDate; + + @Column(name = "energy_consumption_standard", precision = 12, scale = 2) + private BigDecimal energyConsumptionStandard; + + @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 = "special_equipment_type", length = 50) + private String specialEquipmentType; + + @Column(name = "special_equipment_cert", length = 100) + private String specialEquipmentCert; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "attributes", columnDefinition = "text") + private Map attributes; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "photos", columnDefinition = "jsonb") + private List photos; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "documents", columnDefinition = "jsonb") + private List documents; + + @Column(name = "manual_url", length = 500) + private String manualUrl; + + @Column(columnDefinition = "TEXT") + private String remarks; + + @Column(name = "is_deleted") + private Boolean isDeleted = false; + + @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; + + @PrePersist + public void prePersist() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + if (this.status == null) { + this.status = EquipmentStatus.ACTIVE; + } + if (this.ownershipType == null) { + this.ownershipType = OwnershipType.PROJECT; + } + if (this.isDeleted == null) { + this.isDeleted = false; + } + } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + @Data + public static class EquipmentPhoto { + private String type; + private String url; + private String remark; + } + + @Data + public static class EquipmentDocument { + private String name; + private String url; + private Long size; + private String type; + private String remark; + } +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentElevator.java b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentElevator.java new file mode 100644 index 0000000..84375d3 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentElevator.java @@ -0,0 +1,81 @@ +package com.ether.pms.asset.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "asset_equipment_elevator") +@Data +public class EquipmentElevator { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "equipment_id", unique = true, nullable = false) + private UUID equipmentId; + + @Column(name = "elevator_type", length = 30) + private String elevatorType; + + @Column(name = "elevator_model", length = 50) + private String elevatorModel; + + @Column(name = "load_capacity") + private Integer loadCapacity; + + @Column(precision = 10, scale = 2) + private BigDecimal speed; + + @Column(name = "floor_count") + private Integer floorCount; + + @Column(name = "shaft_dimensions", length = 50) + private String shaftDimensions; + + @Column(name = "pit_depth", precision = 10, scale = 2) + private BigDecimal pitDepth; + + @Column(name = "overhead_height", precision = 10, scale = 2) + private BigDecimal overheadHeight; + + @Column(name = "registration_no", length = 50) + private String registrationNo; + + @Column(name = "inspection_certificate", length = 100) + private String inspectionCertificate; + + @Column(name = "next_inspection_date") + private LocalDate nextInspectionDate; + + @Column(name = "energy_consumption", precision = 12, scale = 2) + private BigDecimal energyConsumption; + + @Column(name = "maintenance_level", length = 20) + private String maintenanceLevel; + + @Column(name = "rescue_plan", columnDefinition = "TEXT") + private String rescuePlan; + + @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-asset/src/main/java/com/ether/pms/asset/entity/EquipmentEnergy.java b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentEnergy.java new file mode 100644 index 0000000..d94c19b --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentEnergy.java @@ -0,0 +1,90 @@ +package com.ether.pms.asset.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "asset_equipment_energy") +@Data +public class EquipmentEnergy { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "equipment_id", unique = true, nullable = false) + private UUID equipmentId; + + @Column(name = "meter_type", length = 30) + private String meterType; + + @Column(name = "energy_type", length = 30) + private String energyType; + + @Column(name = "meter_model", length = 50) + private String meterModel; + + @Column(name = "meter_specification", length = 50) + private String meterSpecification; + + @Column(name = "meter_constant", precision = 10, scale = 4) + private BigDecimal meterConstant; + + @Column(name = "accuracy_class", length = 10) + private String accuracyClass; + + @Column(name = "reading_type", length = 20) + private String readingType; + + @Column(name = "last_reading_date") + private LocalDate lastReadingDate; + + @Column(name = "last_reading_value", precision = 12, scale = 2) + private BigDecimal lastReadingValue; + + @Column(name = "current_reading_value", precision = 12, scale = 2) + private BigDecimal currentReadingValue; + + @Column(name = "unit_price", precision = 10, scale = 4) + private BigDecimal unitPrice; + + @Column(name = "billing_type", length = 20) + private String billingType; + + @Column(name = "communication_type", length = 30) + private String communicationType; + + @Column(name = "communication_address", length = 50) + private String communicationAddress; + + @Column(name = "verification_cycle") + private Integer verificationCycle; + + @Column(name = "next_verification_date") + private LocalDate nextVerificationDate; + + @Column(name = "verification_certificate", length = 100) + private String verificationCertificate; + + @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/EquipmentFailureHistory.java b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentFailureHistory.java similarity index 95% rename from module-mdm/src/main/java/com/ether/pms/mdm/entity/EquipmentFailureHistory.java rename to module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentFailureHistory.java index e7aed1b..9ab4a0a 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/entity/EquipmentFailureHistory.java +++ b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentFailureHistory.java @@ -1,4 +1,4 @@ -package com.ether.pms.mdm.entity; +package com.ether.pms.asset.entity; import jakarta.persistence.*; import lombok.Data; @@ -54,7 +54,7 @@ public class EquipmentFailureHistory { @Column(name = "repair_end_time") private LocalDateTime repairEndTime; - @Column(name = "repair_duration_hours", precision = 10, scale = 2) + @Column(name = "repair_duration_hours") private Double repairDurationHours; @Column(name = "repair_person", length = 100) @@ -64,7 +64,7 @@ public class EquipmentFailureHistory { @Enumerated(EnumType.STRING) private RepairResult repairResult; - @Column(name = "downtime_hours", precision = 10, scale = 2) + @Column(name = "downtime_hours") private Double downtimeHours; @Column(name = "maintenance_cost", precision = 12, scale = 2) diff --git a/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentFire.java b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentFire.java new file mode 100644 index 0000000..6c582eb --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentFire.java @@ -0,0 +1,81 @@ +package com.ether.pms.asset.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "asset_equipment_fire") +@Data +public class EquipmentFire { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "equipment_id", unique = true, nullable = false) + private UUID equipmentId; + + @Column(name = "fire_equipment_type", length = 30) + private String fireEquipmentType; + + @Column(name = "installation_area", precision = 10, scale = 2) + private BigDecimal installationArea; + + @Column(name = "installation_height", precision = 10, scale = 2) + private BigDecimal installationHeight; + + @Column(name = "detection_range", precision = 10, scale = 2) + private BigDecimal detectionRange; + + @Column(name = "system_type", length = 30) + private String systemType; + + @Column(name = "zone_number", length = 20) + private String zoneNumber; + + @Column(name = "loop_number", length = 20) + private String loopNumber; + + @Column(name = "linkage_enabled") + private Boolean linkageEnabled; + + @Column(name = "linkage_action", length = 100) + private String linkageAction; + + @Column(name = "inspection_cycle") + private Integer inspectionCycle; + + @Column(name = "last_inspection_date") + private LocalDate lastInspectionDate; + + @Column(name = "next_inspection_date") + private LocalDate nextInspectionDate; + + @Column(name = "inspection_result", length = 20) + private String inspectionResult; + + @Column(name = "special_requirement", columnDefinition = "TEXT") + private String specialRequirement; + + @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/EquipmentHealthScore.java b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentHealthScore.java similarity index 99% rename from module-mdm/src/main/java/com/ether/pms/mdm/entity/EquipmentHealthScore.java rename to module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentHealthScore.java index 12e9264..895641c 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/entity/EquipmentHealthScore.java +++ b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentHealthScore.java @@ -1,4 +1,4 @@ -package com.ether.pms.mdm.entity; +package com.ether.pms.asset.entity; import jakarta.persistence.*; import lombok.Data; @@ -137,4 +137,4 @@ public class EquipmentHealthScore { return CRITICAL; } } -} \ No newline at end of file +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentHvac.java b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentHvac.java new file mode 100644 index 0000000..c8bb89c --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/entity/EquipmentHvac.java @@ -0,0 +1,81 @@ +package com.ether.pms.asset.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "asset_equipment_hvac") +@Data +public class EquipmentHvac { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "equipment_id", unique = true, nullable = false) + private UUID equipmentId; + + @Column(name = "hvac_type", length = 30) + private String hvacType; + + @Column(name = "cooling_capacity", precision = 12, scale = 2) + private BigDecimal coolingCapacity; + + @Column(name = "heating_capacity", precision = 12, scale = 2) + private BigDecimal heatingCapacity; + + @Column(name = "air_flow", precision = 12, scale = 2) + private BigDecimal airFlow; + + @Column(name = "refrigerant_type", length = 30) + private String refrigerantType; + + @Column(name = "refrigerant_charge", precision = 10, scale = 2) + private BigDecimal refrigerantCharge; + + @Column(name = "energy_efficiency_ratio", precision = 10, scale = 2) + private BigDecimal energyEfficiencyRatio; + + @Column(name = "coefficient_of_performance", precision = 10, scale = 2) + private BigDecimal coefficientOfPerformance; + + @Column(name = "installation_date") + private LocalDate installationDate; + + @Column(name = "warranty_expire_date") + private LocalDate warrantyExpireDate; + + @Column(name = "filter_replacement_cycle") + private Integer filterReplacementCycle; + + @Column(name = "last_filter_replacement") + private LocalDate lastFilterReplacement; + + @Column(name = "duct_type", length = 30) + private String ductType; + + @Column(name = "duct_dimensions", length = 50) + private String ductDimensions; + + @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-asset/src/main/java/com/ether/pms/asset/entity/OwnershipEntity.java b/module-asset/src/main/java/com/ether/pms/asset/entity/OwnershipEntity.java new file mode 100644 index 0000000..9532bab --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/entity/OwnershipEntity.java @@ -0,0 +1,104 @@ +package com.ether.pms.asset.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "asset_ownership_entity", indexes = { + @Index(name = "idx_ownership_entity_type", columnList = "entity_type"), + @Index(name = "idx_ownership_entity_code", columnList = "entity_code") +}) +@Data +public class OwnershipEntity { + + public enum EntityType { + COMPANY("公司"), + OWNER("业主"), + RENTAL_COMPANY("租赁公司"); + + private final String description; + + EntityType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + } + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Enumerated(EnumType.STRING) + @Column(name = "entity_type", nullable = false, length = 20) + private EntityType entityType; + + @Column(name = "entity_name", nullable = false, length = 100) + private String entityName; + + @Column(name = "entity_code", length = 50) + private String entityCode; + + @Column(name = "contact_person", length = 50) + private String contactPerson; + + @Column(name = "contact_phone", length = 20) + private String contactPhone; + + @Column(name = "contact_address", length = 255) + private String contactAddress; + + @Column(name = "business_license", length = 50) + private String businessLicense; + + @Column(name = "legal_representative", length = 50) + private String legalRepresentative; + + @Column(name = "contract_no", length = 50) + private String contractNo; + + @Column(name = "contract_start_date") + private LocalDate contractStartDate; + + @Column(name = "contract_end_date") + private LocalDate contractEndDate; + + @Column(name = "rental_fee", precision = 12, scale = 2) + private BigDecimal rentalFee; + + @Column(length = 20) + private String status = "ACTIVE"; + + @Column(name = "is_deleted") + private Boolean isDeleted = false; + + @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.status == null) { + this.status = "ACTIVE"; + } + if (this.isDeleted == null) { + this.isDeleted = false; + } + } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/enums/EquipmentStatus.java b/module-asset/src/main/java/com/ether/pms/asset/enums/EquipmentStatus.java new file mode 100644 index 0000000..b630a59 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/enums/EquipmentStatus.java @@ -0,0 +1,18 @@ +package com.ether.pms.asset.enums; + +public enum EquipmentStatus { + ACTIVE("在用"), + INACTIVE("停用"), + MAINTENANCE("维保中"), + SCRAPPED("已报废"); + + private final String description; + + EquipmentStatus(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/enums/EquipmentType.java b/module-asset/src/main/java/com/ether/pms/asset/enums/EquipmentType.java new file mode 100644 index 0000000..1cc1d36 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/enums/EquipmentType.java @@ -0,0 +1,24 @@ +package com.ether.pms.asset.enums; + +public enum EquipmentType { + ELEVATOR("电梯系统"), + HVAC("暖通空调"), + FIRE_PROTECTION("消防系统"), + PLUMBING("给排水系统"), + ELECTRICAL("电气系统"), + ENERGY_METER("能源计量"), + SECURITY("弱电系统"), + LANDSCAPE("景观绿化"), + KITCHEN("厨余设备"), + OTHER("其他设备"); + + private final String description; + + EquipmentType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/enums/OwnershipType.java b/module-asset/src/main/java/com/ether/pms/asset/enums/OwnershipType.java new file mode 100644 index 0000000..28c73ac --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/enums/OwnershipType.java @@ -0,0 +1,18 @@ +package com.ether.pms.asset.enums; + +public enum OwnershipType { + PROJECT("项目自有"), + COMPANY("公司统筹"), + OWNER("业主自置"), + RENTAL("租赁设备"); + + private final String description; + + OwnershipType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/enums/SystemType.java b/module-asset/src/main/java/com/ether/pms/asset/enums/SystemType.java new file mode 100644 index 0000000..6ad1b4f --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/enums/SystemType.java @@ -0,0 +1,26 @@ +package com.ether.pms.asset.enums; + +/** + * 商业地产8大系统分类枚举 + */ +public enum SystemType { + HVAC("暖通空调"), + FIRE("消防系统"), + ELEVATOR("电梯系统"), + ELECTRICAL("电气系统"), + PLUMBING("给排水"), + BAS("弱电智能化"), + KITCHEN("餐饮厨房"), + LANDSCAPE("景观"), + OTHER("其他"); + + private final String description; + + SystemType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentElevatorRepository.java b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentElevatorRepository.java new file mode 100644 index 0000000..6e9fc75 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentElevatorRepository.java @@ -0,0 +1,16 @@ +package com.ether.pms.asset.repository; + +import com.ether.pms.asset.entity.EquipmentElevator; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface EquipmentElevatorRepository extends JpaRepository { + + Optional findByEquipmentId(UUID equipmentId); + + void deleteByEquipmentId(UUID equipmentId); +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentEnergyRepository.java b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentEnergyRepository.java new file mode 100644 index 0000000..b502725 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentEnergyRepository.java @@ -0,0 +1,16 @@ +package com.ether.pms.asset.repository; + +import com.ether.pms.asset.entity.EquipmentEnergy; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface EquipmentEnergyRepository extends JpaRepository { + + Optional findByEquipmentId(UUID equipmentId); + + void deleteByEquipmentId(UUID equipmentId); +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/EquipmentFailureHistoryRepository.java b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentFailureHistoryRepository.java similarity index 93% rename from module-mdm/src/main/java/com/ether/pms/mdm/repository/EquipmentFailureHistoryRepository.java rename to module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentFailureHistoryRepository.java index 619ff6f..586caf2 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/repository/EquipmentFailureHistoryRepository.java +++ b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentFailureHistoryRepository.java @@ -1,6 +1,6 @@ -package com.ether.pms.mdm.repository; +package com.ether.pms.asset.repository; -import com.ether.pms.mdm.entity.EquipmentFailureHistory; +import com.ether.pms.asset.entity.EquipmentFailureHistory; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentFireRepository.java b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentFireRepository.java new file mode 100644 index 0000000..ea84de6 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentFireRepository.java @@ -0,0 +1,16 @@ +package com.ether.pms.asset.repository; + +import com.ether.pms.asset.entity.EquipmentFire; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface EquipmentFireRepository extends JpaRepository { + + Optional findByEquipmentId(UUID equipmentId); + + void deleteByEquipmentId(UUID equipmentId); +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/EquipmentHealthScoreRepository.java b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentHealthScoreRepository.java similarity index 94% rename from module-mdm/src/main/java/com/ether/pms/mdm/repository/EquipmentHealthScoreRepository.java rename to module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentHealthScoreRepository.java index 7e7b7ae..ee69df6 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/repository/EquipmentHealthScoreRepository.java +++ b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentHealthScoreRepository.java @@ -1,6 +1,6 @@ -package com.ether.pms.mdm.repository; +package com.ether.pms.asset.repository; -import com.ether.pms.mdm.entity.EquipmentHealthScore; +import com.ether.pms.asset.entity.EquipmentHealthScore; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentHvacRepository.java b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentHvacRepository.java new file mode 100644 index 0000000..6405be4 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentHvacRepository.java @@ -0,0 +1,16 @@ +package com.ether.pms.asset.repository; + +import com.ether.pms.asset.entity.EquipmentHvac; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface EquipmentHvacRepository extends JpaRepository { + + Optional findByEquipmentId(UUID equipmentId); + + void deleteByEquipmentId(UUID equipmentId); +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentRepository.java b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentRepository.java new file mode 100644 index 0000000..9c48cd3 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/repository/EquipmentRepository.java @@ -0,0 +1,49 @@ +package com.ether.pms.asset.repository; + +import com.ether.pms.asset.entity.Equipment; +import com.ether.pms.asset.enums.EquipmentStatus; +import com.ether.pms.asset.enums.EquipmentType; +import com.ether.pms.asset.enums.OwnershipType; +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 EquipmentRepository extends JpaRepository { + + List findByProjectIdAndIsDeletedFalse(UUID projectId); + + List findByProjectIdAndStatusAndIsDeletedFalse(UUID projectId, EquipmentStatus status); + + List findBySpaceNodeIdAndIsDeletedFalse(UUID spaceNodeId); + + List findByEquipmentTypeAndIsDeletedFalse(EquipmentType equipmentType); + + List findByOwnershipTypeAndIsDeletedFalse(OwnershipType ownershipType); + + Optional findByEquipmentCode(String equipmentCode); + + Optional findByIdAndIsDeletedFalse(UUID id); + + boolean existsByEquipmentCode(String equipmentCode); + + @Query("SELECT e FROM Equipment e WHERE e.projectId = :projectId AND e.isDeleted = false") + List findAllByProject(@Param("projectId") UUID projectId); + + @Query("SELECT e FROM Equipment e WHERE e.spaceNodeId = :spaceNodeId AND e.isDeleted = false") + List findBySpaceNode(@Param("spaceNodeId") UUID spaceNodeId); + + @Query("SELECT COUNT(e) FROM Equipment e WHERE e.projectId = :projectId AND e.isDeleted = false") + long countByProject(@Param("projectId") UUID projectId); + + @Query("SELECT e.equipmentType, COUNT(e) FROM Equipment e WHERE e.projectId = :projectId AND e.isDeleted = false GROUP BY e.equipmentType") + List countByType(@Param("projectId") UUID projectId); + + @Query("SELECT e.ownershipType, COUNT(e) FROM Equipment e WHERE e.projectId = :projectId AND e.isDeleted = false GROUP BY e.ownershipType") + List countByOwnership(@Param("projectId") UUID projectId); +} \ No newline at end of file diff --git a/module-asset/src/main/java/com/ether/pms/asset/repository/OwnershipEntityRepository.java b/module-asset/src/main/java/com/ether/pms/asset/repository/OwnershipEntityRepository.java new file mode 100644 index 0000000..8ffa346 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/repository/OwnershipEntityRepository.java @@ -0,0 +1,23 @@ +package com.ether.pms.asset.repository; + +import com.ether.pms.asset.entity.OwnershipEntity; +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 OwnershipEntityRepository extends JpaRepository { + + List findByEntityTypeAndIsDeletedFalse(OwnershipEntity.EntityType entityType); + + Optional findByEntityCode(String entityCode); + + Optional findByIdAndIsDeletedFalse(UUID id); + + boolean existsByEntityCode(String entityCode); + + List findByIsDeletedFalse(); +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentElevatorService.java b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentElevatorService.java new file mode 100644 index 0000000..15ae0a2 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentElevatorService.java @@ -0,0 +1,15 @@ +package com.ether.pms.asset.service; + +import com.ether.pms.asset.entity.EquipmentElevator; + +import java.util.Optional; +import java.util.UUID; + +public interface EquipmentElevatorService { + + EquipmentElevator saveOrUpdate(EquipmentElevator elevator); + + Optional getByEquipmentId(UUID equipmentId); + + void deleteByEquipmentId(UUID equipmentId); +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentEnergyService.java b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentEnergyService.java new file mode 100644 index 0000000..f8dd086 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentEnergyService.java @@ -0,0 +1,15 @@ +package com.ether.pms.asset.service; + +import com.ether.pms.asset.entity.EquipmentEnergy; + +import java.util.Optional; +import java.util.UUID; + +public interface EquipmentEnergyService { + + EquipmentEnergy saveOrUpdate(EquipmentEnergy energy); + + Optional getByEquipmentId(UUID equipmentId); + + void deleteByEquipmentId(UUID equipmentId); +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentFireService.java b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentFireService.java new file mode 100644 index 0000000..82f6eb2 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentFireService.java @@ -0,0 +1,15 @@ +package com.ether.pms.asset.service; + +import com.ether.pms.asset.entity.EquipmentFire; + +import java.util.Optional; +import java.util.UUID; + +public interface EquipmentFireService { + + EquipmentFire saveOrUpdate(EquipmentFire fire); + + Optional getByEquipmentId(UUID equipmentId); + + void deleteByEquipmentId(UUID equipmentId); +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/EquipmentHealthService.java b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentHealthService.java similarity index 89% rename from module-mdm/src/main/java/com/ether/pms/mdm/service/EquipmentHealthService.java rename to module-asset/src/main/java/com/ether/pms/asset/service/EquipmentHealthService.java index 590df52..14869a5 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/service/EquipmentHealthService.java +++ b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentHealthService.java @@ -1,7 +1,7 @@ -package com.ether.pms.mdm.service; +package com.ether.pms.asset.service; -import com.ether.pms.mdm.entity.EquipmentFailureHistory; -import com.ether.pms.mdm.entity.EquipmentHealthScore; +import com.ether.pms.asset.entity.EquipmentFailureHistory; +import com.ether.pms.asset.entity.EquipmentHealthScore; import java.math.BigDecimal; import java.util.List; import java.util.UUID; diff --git a/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentHvacService.java b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentHvacService.java new file mode 100644 index 0000000..9a6513a --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentHvacService.java @@ -0,0 +1,15 @@ +package com.ether.pms.asset.service; + +import com.ether.pms.asset.entity.EquipmentHvac; + +import java.util.Optional; +import java.util.UUID; + +public interface EquipmentHvacService { + + EquipmentHvac saveOrUpdate(EquipmentHvac hvac); + + Optional getByEquipmentId(UUID equipmentId); + + void deleteByEquipmentId(UUID equipmentId); +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentService.java b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentService.java new file mode 100644 index 0000000..7fe08a2 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/service/EquipmentService.java @@ -0,0 +1,41 @@ +package com.ether.pms.asset.service; + +import com.ether.pms.asset.entity.Equipment; +import com.ether.pms.asset.enums.EquipmentType; +import com.ether.pms.asset.enums.OwnershipType; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public interface EquipmentService { + + Equipment createEquipment(Equipment equipment); + + Equipment updateEquipment(UUID id, Equipment equipment); + + void deleteEquipment(UUID id); + + Equipment getEquipmentById(UUID id); + + List getEquipmentsByProject(UUID projectId); + + List getEquipmentsBySpaceNode(UUID spaceNodeId); + + List getEquipmentsByType(EquipmentType equipmentType); + + List getEquipmentsByOwnership(OwnershipType ownershipType); + + Map getEquipmentStatsByType(UUID projectId); + + Map getEquipmentStatsByOwnership(UUID projectId); + + long countByProject(UUID projectId); + + void deleteEquipmentBatch(List ids); + + Map importFromExcel(MultipartFile file, UUID projectId); + + byte[] exportToExcel(UUID projectId); +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentElevatorServiceImpl.java b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentElevatorServiceImpl.java new file mode 100644 index 0000000..6018fdc --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentElevatorServiceImpl.java @@ -0,0 +1,39 @@ +package com.ether.pms.asset.service.impl; + +import com.ether.pms.asset.entity.EquipmentElevator; +import com.ether.pms.asset.repository.EquipmentElevatorRepository; +import com.ether.pms.asset.service.EquipmentElevatorService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class EquipmentElevatorServiceImpl implements EquipmentElevatorService { + + private final EquipmentElevatorRepository elevatorRepository; + + @Override + @Transactional + public EquipmentElevator saveOrUpdate(EquipmentElevator elevator) { + Optional existing = elevatorRepository.findByEquipmentId(elevator.getEquipmentId()); + if (existing.isPresent()) { + elevator.setId(existing.get().getId()); + } + return elevatorRepository.save(elevator); + } + + @Override + public Optional getByEquipmentId(UUID equipmentId) { + return elevatorRepository.findByEquipmentId(equipmentId); + } + + @Override + @Transactional + public void deleteByEquipmentId(UUID equipmentId) { + elevatorRepository.deleteByEquipmentId(equipmentId); + } +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentEnergyServiceImpl.java b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentEnergyServiceImpl.java new file mode 100644 index 0000000..70a4fb1 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentEnergyServiceImpl.java @@ -0,0 +1,39 @@ +package com.ether.pms.asset.service.impl; + +import com.ether.pms.asset.entity.EquipmentEnergy; +import com.ether.pms.asset.repository.EquipmentEnergyRepository; +import com.ether.pms.asset.service.EquipmentEnergyService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class EquipmentEnergyServiceImpl implements EquipmentEnergyService { + + private final EquipmentEnergyRepository energyRepository; + + @Override + @Transactional + public EquipmentEnergy saveOrUpdate(EquipmentEnergy energy) { + Optional existing = energyRepository.findByEquipmentId(energy.getEquipmentId()); + if (existing.isPresent()) { + energy.setId(existing.get().getId()); + } + return energyRepository.save(energy); + } + + @Override + public Optional getByEquipmentId(UUID equipmentId) { + return energyRepository.findByEquipmentId(equipmentId); + } + + @Override + @Transactional + public void deleteByEquipmentId(UUID equipmentId) { + energyRepository.deleteByEquipmentId(equipmentId); + } +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentFireServiceImpl.java b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentFireServiceImpl.java new file mode 100644 index 0000000..b4e7528 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentFireServiceImpl.java @@ -0,0 +1,39 @@ +package com.ether.pms.asset.service.impl; + +import com.ether.pms.asset.entity.EquipmentFire; +import com.ether.pms.asset.repository.EquipmentFireRepository; +import com.ether.pms.asset.service.EquipmentFireService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class EquipmentFireServiceImpl implements EquipmentFireService { + + private final EquipmentFireRepository fireRepository; + + @Override + @Transactional + public EquipmentFire saveOrUpdate(EquipmentFire fire) { + Optional existing = fireRepository.findByEquipmentId(fire.getEquipmentId()); + if (existing.isPresent()) { + fire.setId(existing.get().getId()); + } + return fireRepository.save(fire); + } + + @Override + public Optional getByEquipmentId(UUID equipmentId) { + return fireRepository.findByEquipmentId(equipmentId); + } + + @Override + @Transactional + public void deleteByEquipmentId(UUID equipmentId) { + fireRepository.deleteByEquipmentId(equipmentId); + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/EquipmentHealthServiceImpl.java b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentHealthServiceImpl.java similarity index 85% rename from module-mdm/src/main/java/com/ether/pms/mdm/service/impl/EquipmentHealthServiceImpl.java rename to module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentHealthServiceImpl.java index 110ead9..ac3786b 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/EquipmentHealthServiceImpl.java +++ b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentHealthServiceImpl.java @@ -1,15 +1,13 @@ -package com.ether.pms.mdm.service.impl; +package com.ether.pms.asset.service.impl; import com.ether.pms.common.BusinessException; -import com.ether.pms.mdm.entity.EquipmentFailureHistory; -import com.ether.pms.mdm.entity.EquipmentHealthScore; -import com.ether.pms.mdm.entity.MaintenanceTask; +import com.ether.pms.asset.entity.EquipmentFailureHistory; +import com.ether.pms.asset.entity.EquipmentHealthScore; import com.ether.pms.mdm.entity.SpaceNode; -import com.ether.pms.mdm.repository.EquipmentFailureHistoryRepository; -import com.ether.pms.mdm.repository.EquipmentHealthScoreRepository; -import com.ether.pms.mdm.repository.MaintenanceTaskRepository; +import com.ether.pms.asset.repository.EquipmentFailureHistoryRepository; +import com.ether.pms.asset.repository.EquipmentHealthScoreRepository; import com.ether.pms.mdm.repository.SpaceNodeRepository; -import com.ether.pms.mdm.service.EquipmentHealthService; +import com.ether.pms.asset.service.EquipmentHealthService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -32,7 +30,8 @@ public class EquipmentHealthServiceImpl implements EquipmentHealthService { private final EquipmentFailureHistoryRepository failureHistoryRepository; private final EquipmentHealthScoreRepository healthScoreRepository; private final SpaceNodeRepository spaceNodeRepository; - private final MaintenanceTaskRepository maintenanceTaskRepository; + // TODO: 需要改为从 ops 模块查询工单数据 + // private final MaintenanceTaskRepository maintenanceTaskRepository; private static final BigDecimal BASE_SCORE = new BigDecimal("100"); private static final BigDecimal FAILURE_DEDUCTION_PER_COUNT = new BigDecimal("5"); @@ -58,20 +57,10 @@ public class EquipmentHealthServiceImpl implements EquipmentHealthService { // 计算故障率扣分 BigDecimal failureDeduction = FAILURE_DEDUCTION_PER_COUNT.multiply(BigDecimal.valueOf(failureCount30d)); - // 计算维保完成率 - List tasks = maintenanceTaskRepository.findByEquipmentIdAndStatusNot(equipmentId, MaintenanceTask.Status.CANCELLED); - long totalTasks = tasks.size(); - long completedTasks = tasks.stream() - .filter(t -> t.getStatus() == MaintenanceTask.Status.COMPLETED) - .count(); - + // TODO: 从 ops 模块查询工单数据计算维保完成率 + // 暂时跳过维保完成率计算 BigDecimal maintenanceCompletionRate = BigDecimal.ONE; - if (totalTasks > 0) { - maintenanceCompletionRate = BigDecimal.valueOf(completedTasks) - .divide(BigDecimal.valueOf(totalTasks), 4, RoundingMode.HALF_UP); - } - BigDecimal maintenanceDeduction = BigDecimal.ONE.subtract(maintenanceCompletionRate) - .multiply(MAINTENANCE_DEDUCTION_FACTOR); + BigDecimal maintenanceDeduction = BigDecimal.ZERO; // 计算年龄扣分 BigDecimal equipmentAgeYears = calculateEquipmentAge(equipment); @@ -97,9 +86,8 @@ public class EquipmentHealthServiceImpl implements EquipmentHealthService { // 创建健康度记录 EquipmentHealthScore health = new EquipmentHealthScore(); - health.setProjectId(equipment.getProjectId()); + health.setProjectId(UUID.fromString("00000000-0000-0000-0000-000000000000")); // 需要根据projectCode查询 health.setEquipmentId(equipmentId); - health.setEquipmentCode(equipment.getCode()); health.setEquipmentName(equipment.getName()); health.setHealthScore(healthScore.setScale(2, RoundingMode.HALF_UP)); health.setFailureDeduction(failureDeduction.setScale(2, RoundingMode.HALF_UP)); @@ -188,13 +176,10 @@ public class EquipmentHealthServiceImpl implements EquipmentHealthService { // 设置项目ID if (failure.getProjectId() == null) { - failure.setProjectId(equipment.getProjectId()); + failure.setProjectId(UUID.fromString("00000000-0000-0000-0000-000000000000")); // 需要根据projectCode查询 } // 设置设备信息 - if (failure.getEquipmentCode() == null) { - failure.setEquipmentCode(equipment.getCode()); - } if (failure.getEquipmentName() == null) { failure.setEquipmentName(equipment.getName()); } diff --git a/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentHvacServiceImpl.java b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentHvacServiceImpl.java new file mode 100644 index 0000000..ac9ecab --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentHvacServiceImpl.java @@ -0,0 +1,39 @@ +package com.ether.pms.asset.service.impl; + +import com.ether.pms.asset.entity.EquipmentHvac; +import com.ether.pms.asset.repository.EquipmentHvacRepository; +import com.ether.pms.asset.service.EquipmentHvacService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class EquipmentHvacServiceImpl implements EquipmentHvacService { + + private final EquipmentHvacRepository hvacRepository; + + @Override + @Transactional + public EquipmentHvac saveOrUpdate(EquipmentHvac hvac) { + Optional existing = hvacRepository.findByEquipmentId(hvac.getEquipmentId()); + if (existing.isPresent()) { + hvac.setId(existing.get().getId()); + } + return hvacRepository.save(hvac); + } + + @Override + public Optional getByEquipmentId(UUID equipmentId) { + return hvacRepository.findByEquipmentId(equipmentId); + } + + @Override + @Transactional + public void deleteByEquipmentId(UUID equipmentId) { + hvacRepository.deleteByEquipmentId(equipmentId); + } +} diff --git a/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentServiceImpl.java b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentServiceImpl.java new file mode 100644 index 0000000..082baa9 --- /dev/null +++ b/module-asset/src/main/java/com/ether/pms/asset/service/impl/EquipmentServiceImpl.java @@ -0,0 +1,405 @@ +package com.ether.pms.asset.service.impl; + +import com.ether.pms.common.BusinessException; +import com.ether.pms.common.ErrorCode; +import com.ether.pms.asset.entity.Equipment; +import com.ether.pms.asset.enums.EquipmentStatus; +import com.ether.pms.asset.enums.EquipmentType; +import com.ether.pms.asset.enums.OwnershipType; +import com.ether.pms.asset.enums.SystemType; +import com.ether.pms.asset.repository.EquipmentRepository; +import com.ether.pms.asset.service.EquipmentService; +import lombok.RequiredArgsConstructor; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class EquipmentServiceImpl implements EquipmentService { + + private final EquipmentRepository equipmentRepository; + + @Override + @Transactional + public Equipment createEquipment(Equipment equipment) { + if (equipment.getEquipmentCode() == null || equipment.getEquipmentCode().isEmpty()) { + equipment.setEquipmentCode(generateEquipmentCode()); + } + if (equipment.getStatus() == null) { + equipment.setStatus(EquipmentStatus.ACTIVE); + } + if (equipment.getOwnershipType() == null) { + equipment.setOwnershipType(OwnershipType.PROJECT); + } + return equipmentRepository.save(equipment); + } + + @Override + @Transactional + public Equipment updateEquipment(UUID id, Equipment equipment) { + Equipment existing = equipmentRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "设备不存在")); + updateFields(existing, equipment); + return equipmentRepository.save(existing); + } + + @Override + @Transactional + public void deleteEquipment(UUID id) { + Equipment equipment = equipmentRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "设备不存在")); + equipment.setIsDeleted(true); + equipment.setStatus(EquipmentStatus.INACTIVE); + equipmentRepository.save(equipment); + } + + @Override + public Equipment getEquipmentById(UUID id) { + return equipmentRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "设备不存在")); + } + + @Override + public List getEquipmentsByProject(UUID projectId) { + return equipmentRepository.findByProjectIdAndIsDeletedFalse(projectId); + } + + @Override + public List getEquipmentsBySpaceNode(UUID spaceNodeId) { + return equipmentRepository.findBySpaceNodeIdAndIsDeletedFalse(spaceNodeId); + } + + @Override + public List getEquipmentsByType(EquipmentType equipmentType) { + return equipmentRepository.findByEquipmentTypeAndIsDeletedFalse(equipmentType); + } + + @Override + public List getEquipmentsByOwnership(OwnershipType ownershipType) { + return equipmentRepository.findByOwnershipTypeAndIsDeletedFalse(ownershipType); + } + + @Override + public Map getEquipmentStatsByType(UUID projectId) { + List results = equipmentRepository.countByType(projectId); + Map stats = new HashMap<>(); + for (Object[] result : results) { + EquipmentType type = (EquipmentType) result[0]; + Long count = (Long) result[1]; + stats.put(type.name(), count); + } + return stats; + } + + @Override + public Map getEquipmentStatsByOwnership(UUID projectId) { + List results = equipmentRepository.countByOwnership(projectId); + Map stats = new HashMap<>(); + for (Object[] result : results) { + OwnershipType type = (OwnershipType) result[0]; + Long count = (Long) result[1]; + stats.put(type.name(), count); + } + return stats; + } + + @Override + public long countByProject(UUID projectId) { + return equipmentRepository.countByProject(projectId); + } + + private String generateEquipmentCode() { + return "EQC-" + LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + } + + private void updateFields(Equipment existing, Equipment updated) { + existing.setEquipmentName(updated.getEquipmentName()); + existing.setEquipmentType(updated.getEquipmentType()); + existing.setEquipmentCategory(updated.getEquipmentCategory()); + existing.setSystemType(updated.getSystemType()); + existing.setOwnershipType(updated.getOwnershipType()); + existing.setOwningEntityId(updated.getOwningEntityId()); + existing.setOwningEntityName(updated.getOwningEntityName()); + existing.setAssetCode(updated.getAssetCode()); + existing.setSerialNumber(updated.getSerialNumber()); + existing.setModel(updated.getModel()); + existing.setManufacturer(updated.getManufacturer()); + existing.setSupplier(updated.getSupplier()); + existing.setStatus(updated.getStatus()); + existing.setOperationStatus(updated.getOperationStatus()); + existing.setInstallationLocation(updated.getInstallationLocation()); + existing.setInstallationDate(updated.getInstallationDate()); + existing.setDesignLifeYears(updated.getDesignLifeYears()); + existing.setRatedPower(updated.getRatedPower()); + existing.setRatedVoltage(updated.getRatedVoltage()); + existing.setRatedCurrent(updated.getRatedCurrent()); + existing.setMaintenanceVendor(updated.getMaintenanceVendor()); + existing.setMaintenanceVendorContact(updated.getMaintenanceVendorContact()); + existing.setMaintenanceVendorPhone(updated.getMaintenanceVendorPhone()); + existing.setMaintenanceContractNo(updated.getMaintenanceContractNo()); + existing.setMaintenanceContractStart(updated.getMaintenanceContractStart()); + existing.setMaintenanceContractEnd(updated.getMaintenanceContractEnd()); + existing.setEnergyConsumptionStandard(updated.getEnergyConsumptionStandard()); + existing.setInspectionCycle(updated.getInspectionCycle()); + existing.setNextInspectionDate(updated.getNextInspectionDate()); + existing.setLastInspectionDate(updated.getLastInspectionDate()); + existing.setLastInspectionResult(updated.getLastInspectionResult()); + existing.setSpecialEquipmentType(updated.getSpecialEquipmentType()); + existing.setSpecialEquipmentCert(updated.getSpecialEquipmentCert()); + existing.setAttributes(updated.getAttributes()); + existing.setRemarks(updated.getRemarks()); + } + + @Override + @Transactional + public void deleteEquipmentBatch(List ids) { + for (UUID id : ids) { + try { + deleteEquipment(id); + } catch (Exception e) { + throw new BusinessException(ErrorCode.BAD_REQUEST, "删除设备失败: " + id); + } + } + } + + @Override + @Transactional + public Map importFromExcel(MultipartFile file, UUID projectId) { + List successList = new ArrayList<>(); + List failList = new ArrayList<>(); + + try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) { + Sheet sheet = workbook.getSheetAt(0); + if (sheet == null) { + throw new BusinessException(ErrorCode.BAD_REQUEST, "Excel文件为空"); + } + + for (int i = 1; i <= sheet.getLastRowNum(); i++) { + Row row = sheet.getRow(i); + if (row == null || isRowEmpty(row)) continue; + + try { + Equipment equipment = parseEquipmentFromRow(row, projectId); + equipmentRepository.save(equipment); + successList.add(equipment.getEquipmentName()); + } catch (Exception e) { + failList.add("第" + (i + 1) + "行: " + e.getMessage()); + } + } + } catch (IOException e) { + throw new BusinessException(ErrorCode.BAD_REQUEST, "文件解析失败: " + e.getMessage()); + } + + Map result = new HashMap<>(); + result.put("successCount", successList.size()); + result.put("failCount", failList.size()); + result.put("failDetails", failList); + return result; + } + + @Override + public byte[] exportToExcel(UUID projectId) { + List equipmentList = equipmentRepository.findByProjectIdAndIsDeletedFalse(projectId); + + try (Workbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Sheet sheet = workbook.createSheet("设备列表"); + + String[] headers = {"设备编码", "设备名称", "设备类型", "系统类型", "归属类型", + "型号", "厂商", "额定功率(kW)", "额定电压(V)", "安装位置", + "维保商", "维保电话", "年检周期(月)", "购置日期", "购置价格", "保修到期"}; + Row headerRow = sheet.createRow(0); + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + cell.setCellStyle(createHeaderStyle(workbook)); + } + + int rowNum = 1; + for (Equipment eq : equipmentList) { + Row row = sheet.createRow(rowNum++); + row.createCell(0).setCellValue(eq.getEquipmentCode() != null ? eq.getEquipmentCode() : ""); + row.createCell(1).setCellValue(eq.getEquipmentName() != null ? eq.getEquipmentName() : ""); + row.createCell(2).setCellValue(eq.getEquipmentType() != null ? eq.getEquipmentType().getDescription() : ""); + row.createCell(3).setCellValue(eq.getSystemType() != null ? eq.getSystemType().getDescription() : ""); + row.createCell(4).setCellValue(eq.getOwnershipType() != null ? eq.getOwnershipType().getDescription() : ""); + row.createCell(5).setCellValue(eq.getModel() != null ? eq.getModel() : ""); + row.createCell(6).setCellValue(eq.getManufacturer() != null ? eq.getManufacturer() : ""); + row.createCell(7).setCellValue(eq.getRatedPower() != null ? eq.getRatedPower().doubleValue() : 0); + row.createCell(8).setCellValue(eq.getRatedVoltage() != null ? eq.getRatedVoltage() : ""); + row.createCell(9).setCellValue(eq.getInstallationLocation() != null ? eq.getInstallationLocation() : ""); + row.createCell(10).setCellValue(eq.getMaintenanceVendor() != null ? eq.getMaintenanceVendor() : ""); + row.createCell(11).setCellValue(eq.getMaintenanceVendorPhone() != null ? eq.getMaintenanceVendorPhone() : ""); + row.createCell(12).setCellValue(eq.getInspectionCycle() != null ? eq.getInspectionCycle() : 0); + row.createCell(13).setCellValue(eq.getPurchaseDate() != null ? eq.getPurchaseDate().toString() : ""); + row.createCell(14).setCellValue(eq.getPurchasePrice() != null ? eq.getPurchasePrice().doubleValue() : 0); + row.createCell(15).setCellValue(eq.getWarrantyExpireDate() != null ? eq.getWarrantyExpireDate().toString() : ""); + } + + for (int i = 0; i < headers.length; i++) { + sheet.autoSizeColumn(i); + } + + workbook.write(out); + return out.toByteArray(); + } catch (IOException e) { + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "导出Excel失败"); + } + } + + private Equipment parseEquipmentFromRow(Row row, UUID projectId) { + Equipment equipment = new Equipment(); + equipment.setProjectId(projectId); + equipment.setEquipmentName(getCellStringValue(row.getCell(0))); + equipment.setEquipmentCode(getCellStringValue(row.getCell(1))); + equipment.setEquipmentType(parseEquipmentType(getCellStringValue(row.getCell(2)))); + equipment.setSystemType(parseSystemType(getCellStringValue(row.getCell(3)))); + equipment.setOwnershipType(parseOwnershipType(getCellStringValue(row.getCell(4)))); + equipment.setModel(getCellStringValue(row.getCell(5))); + equipment.setManufacturer(getCellStringValue(row.getCell(6))); + equipment.setRatedPower(getCellNumericValue(row.getCell(7))); + equipment.setRatedVoltage(getCellStringValue(row.getCell(8))); + equipment.setInstallationLocation(getCellStringValue(row.getCell(9))); + equipment.setMaintenanceVendor(getCellStringValue(row.getCell(10))); + equipment.setMaintenanceVendorPhone(getCellStringValue(row.getCell(11))); + equipment.setInspectionCycle(getCellIntegerValue(row.getCell(12))); + equipment.setPurchaseDate(getCellDateValue(row.getCell(13))); + equipment.setPurchasePrice(getCellNumericValue(row.getCell(14))); + equipment.setWarrantyExpireDate(getCellDateValue(row.getCell(15))); + equipment.setStatus(EquipmentStatus.ACTIVE); + if (equipment.getEquipmentCode() == null || equipment.getEquipmentCode().isEmpty()) { + equipment.setEquipmentCode(generateEquipmentCode()); + } + if (equipment.getOwnershipType() == null) { + equipment.setOwnershipType(OwnershipType.PROJECT); + } + return equipment; + } + + private String getCellStringValue(Cell cell) { + if (cell == null) return null; + return switch (cell.getCellType()) { + case STRING -> cell.getStringCellValue().trim(); + case NUMERIC -> String.valueOf((long) cell.getNumericCellValue()); + case BOOLEAN -> String.valueOf(cell.getBooleanCellValue()); + default -> null; + }; + } + + private BigDecimal getCellNumericValue(Cell cell) { + if (cell == null) return null; + return switch (cell.getCellType()) { + case NUMERIC -> BigDecimal.valueOf(cell.getNumericCellValue()); + case STRING -> { + try { + yield new BigDecimal(cell.getStringCellValue().trim()); + } catch (NumberFormatException e) { + yield null; + } + } + default -> null; + }; + } + + private Integer getCellIntegerValue(Cell cell) { + if (cell == null) return null; + return switch (cell.getCellType()) { + case NUMERIC -> (int) cell.getNumericCellValue(); + case STRING -> { + try { + yield Integer.parseInt(cell.getStringCellValue().trim()); + } catch (NumberFormatException e) { + yield null; + } + } + default -> null; + }; + } + + private LocalDate getCellDateValue(Cell cell) { + if (cell == null) return null; + return switch (cell.getCellType()) { + case STRING -> { + String val = cell.getStringCellValue().trim(); + if (!val.isEmpty()) { + try { + yield LocalDate.parse(val); + } catch (Exception e) { + yield null; + } + } else { + yield null; + } + } + default -> null; + }; + } + + private boolean isRowEmpty(Row row) { + for (int i = 0; i < 5; i++) { + Cell cell = row.getCell(i); + if (cell != null && cell.getCellType() != CellType.BLANK) { + String val = getCellStringValue(cell); + if (val != null && !val.isEmpty()) { + return false; + } + } + } + return true; + } + + private EquipmentType parseEquipmentType(String value) { + if (value == null || value.isEmpty()) return null; + for (EquipmentType type : EquipmentType.values()) { + if (type.getDescription().equals(value) || type.name().equalsIgnoreCase(value)) { + return type; + } + } + return null; + } + + private SystemType parseSystemType(String value) { + if (value == null || value.isEmpty()) return null; + for (SystemType type : SystemType.values()) { + if (type.getDescription().equals(value) || type.name().equalsIgnoreCase(value)) { + return type; + } + } + return null; + } + + private OwnershipType parseOwnershipType(String value) { + if (value == null || value.isEmpty()) return OwnershipType.PROJECT; + for (OwnershipType type : OwnershipType.values()) { + if (type.getDescription().equals(value) || type.name().equalsIgnoreCase(value)) { + return type; + } + } + return OwnershipType.PROJECT; + } + + private CellStyle createHeaderStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderTop(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + return style; + } +} diff --git a/module-asset/src/test/java/com/ether/pms/asset/TestApplication.java b/module-asset/src/test/java/com/ether/pms/asset/TestApplication.java new file mode 100644 index 0000000..15ec8d4 --- /dev/null +++ b/module-asset/src/test/java/com/ether/pms/asset/TestApplication.java @@ -0,0 +1,7 @@ +package com.ether.pms.asset; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TestApplication { +} \ No newline at end of file diff --git a/module-asset/src/test/java/com/ether/pms/asset/repository/EquipmentRepositoryTest.java b/module-asset/src/test/java/com/ether/pms/asset/repository/EquipmentRepositoryTest.java new file mode 100644 index 0000000..04c2c08 --- /dev/null +++ b/module-asset/src/test/java/com/ether/pms/asset/repository/EquipmentRepositoryTest.java @@ -0,0 +1,238 @@ +package com.ether.pms.asset.repository; + +import com.ether.pms.asset.TestApplication; +import com.ether.pms.asset.entity.Equipment; +import com.ether.pms.asset.enums.EquipmentStatus; +import com.ether.pms.asset.enums.EquipmentType; +import com.ether.pms.asset.enums.OwnershipType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ContextConfiguration; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * EquipmentRepository 测试类 - TDD方式 + * + * 使用 @DataJpaTest 切片测试,只加载 JPA 相关组件 + */ +@DataJpaTest +@ContextConfiguration(classes = TestApplication.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class EquipmentRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private EquipmentRepository equipmentRepository; + + private UUID projectId; + private UUID spaceNodeId; + private Equipment testEquipment; + + @BeforeEach + void setUp() { + projectId = UUID.randomUUID(); + spaceNodeId = UUID.randomUUID(); + + testEquipment = new Equipment(); + testEquipment.setProjectId(projectId); + testEquipment.setSpaceNodeId(spaceNodeId); + testEquipment.setEquipmentName("测试设备"); + testEquipment.setEquipmentCode("EQ-001"); + testEquipment.setEquipmentType(EquipmentType.ELEVATOR); + testEquipment.setOwnershipType(OwnershipType.PROJECT); + testEquipment.setStatus(EquipmentStatus.ACTIVE); + testEquipment.setIsDeleted(false); + } + + @Test + void save_shouldPersistEquipment() { + Equipment saved = equipmentRepository.save(testEquipment); + entityManager.flush(); + + assertNotNull(saved.getId()); + assertEquals("测试设备", saved.getEquipmentName()); + assertEquals("EQ-001", saved.getEquipmentCode()); + } + + @Test + void findById_shouldReturnEquipment() { + entityManager.persist(testEquipment); + entityManager.flush(); + + Optional found = equipmentRepository.findById(testEquipment.getId()); + + assertTrue(found.isPresent()); + assertEquals("测试设备", found.get().getEquipmentName()); + } + + @Test + void findById_withDeletedEquipment_shouldReturnEmpty() { + testEquipment.setIsDeleted(true); + entityManager.persist(testEquipment); + entityManager.flush(); + + Optional found = equipmentRepository.findByIdAndIsDeletedFalse(testEquipment.getId()); + + assertTrue(found.isEmpty()); + } + + @Test + void findByIdAndIsDeletedFalse_shouldReturnNonDeletedEquipment() { + entityManager.persist(testEquipment); + entityManager.flush(); + + Optional found = equipmentRepository.findByIdAndIsDeletedFalse(testEquipment.getId()); + + assertTrue(found.isPresent()); + assertFalse(found.get().getIsDeleted()); + } + + @Test + void delete_shouldSoftDeleteEquipment() { + entityManager.persist(testEquipment); + entityManager.flush(); + + testEquipment.setIsDeleted(true); + equipmentRepository.save(testEquipment); + entityManager.flush(); + + Optional found = equipmentRepository.findByIdAndIsDeletedFalse(testEquipment.getId()); + assertTrue(found.isEmpty()); + } + + @Test + void findByProjectIdAndIsDeletedFalse_shouldReturnEquipments() { + Equipment equipment1 = createEquipment("设备1", "EQ-001"); + Equipment equipment2 = createEquipment("设备2", "EQ-002"); + entityManager.persist(equipment1); + entityManager.persist(equipment2); + entityManager.flush(); + + List result = equipmentRepository.findByProjectIdAndIsDeletedFalse(projectId); + + assertEquals(2, result.size()); + } + + @Test + void findByProjectIdAndIsDeletedFalse_withDifferentProject_shouldReturnEmpty() { + Equipment equipment = createEquipment("设备1", "EQ-001"); + entityManager.persist(equipment); + entityManager.flush(); + + List result = equipmentRepository.findByProjectIdAndIsDeletedFalse(UUID.randomUUID()); + + assertTrue(result.isEmpty()); + } + + @Test + void findByEquipmentTypeAndIsDeletedFalse_shouldReturnEquipmentsByType() { + Equipment elevator = createEquipment("电梯", "EQ-EL-001"); + elevator.setEquipmentType(EquipmentType.ELEVATOR); + + Equipment hvac = createEquipment("空调", "EQ-HV-001"); + hvac.setEquipmentType(EquipmentType.HVAC); + + entityManager.persist(elevator); + entityManager.persist(hvac); + entityManager.flush(); + + List elevators = equipmentRepository.findByEquipmentTypeAndIsDeletedFalse(EquipmentType.ELEVATOR); + + assertEquals(1, elevators.size()); + assertEquals(EquipmentType.ELEVATOR, elevators.get(0).getEquipmentType()); + } + + @Test + void findByOwnershipTypeAndIsDeletedFalse_shouldReturnEquipmentsByOwnership() { + Equipment projectOwned = createEquipment("项目自有设备", "EQ-PJ-001"); + projectOwned.setOwnershipType(OwnershipType.PROJECT); + + Equipment ownerOwned = createEquipment("业主设备", "EQ-OW-001"); + ownerOwned.setOwnershipType(OwnershipType.OWNER); + + entityManager.persist(projectOwned); + entityManager.persist(ownerOwned); + entityManager.flush(); + + List projectOwnedEquipments = equipmentRepository.findByOwnershipTypeAndIsDeletedFalse(OwnershipType.PROJECT); + + assertEquals(1, projectOwnedEquipments.size()); + assertEquals(OwnershipType.PROJECT, projectOwnedEquipments.get(0).getOwnershipType()); + } + + @Test + void findBySpaceNodeIdAndIsDeletedFalse_shouldReturnEquipmentsBySpace() { + Equipment equipment1 = createEquipment("位置1设备", "EQ-SP-001"); + equipment1.setSpaceNodeId(spaceNodeId); + + entityManager.persist(equipment1); + entityManager.flush(); + + List result = equipmentRepository.findBySpaceNodeIdAndIsDeletedFalse(spaceNodeId); + + assertEquals(1, result.size()); + assertEquals(spaceNodeId, result.get(0).getSpaceNodeId()); + } + + @Test + void countByProject_shouldReturnCorrectCount() { + entityManager.persist(createEquipment("设备1", "EQ-001")); + entityManager.persist(createEquipment("设备2", "EQ-002")); + entityManager.flush(); + + long count = equipmentRepository.countByProject(projectId); + + assertEquals(2, count); + } + + @Test + void existsByEquipmentCode_withExistingCode_shouldReturnTrue() { + entityManager.persist(testEquipment); + entityManager.flush(); + + boolean exists = equipmentRepository.existsByEquipmentCode("EQ-001"); + + assertTrue(exists); + } + + @Test + void existsByEquipmentCode_withNonExistingCode_shouldReturnFalse() { + boolean exists = equipmentRepository.existsByEquipmentCode("NON-EXISTENT"); + + assertFalse(exists); + } + + @Test + void findByEquipmentCode_shouldReturnEquipment() { + entityManager.persist(testEquipment); + entityManager.flush(); + + Optional found = equipmentRepository.findByEquipmentCode("EQ-001"); + + assertTrue(found.isPresent()); + assertEquals("EQ-001", found.get().getEquipmentCode()); + } + + private Equipment createEquipment(String name, String code) { + Equipment equipment = new Equipment(); + equipment.setProjectId(projectId); + equipment.setEquipmentName(name); + equipment.setEquipmentCode(code); + equipment.setEquipmentType(EquipmentType.OTHER); + equipment.setOwnershipType(OwnershipType.PROJECT); + equipment.setStatus(EquipmentStatus.ACTIVE); + equipment.setIsDeleted(false); + return equipment; + } +} \ No newline at end of file diff --git a/module-asset/src/test/resources/application-test.yml b/module-asset/src/test/resources/application-test.yml new file mode 100644 index 0000000..2220cb1 --- /dev/null +++ b/module-asset/src/test/resources/application-test.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + driver-class-name: org.h2.Driver + + jpa: + hibernate: + ddl-auto: create-drop + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + + h2: + console: + enabled: false \ No newline at end of file diff --git a/module-asset/src/test/resources/application.yml b/module-asset/src/test/resources/application.yml new file mode 100644 index 0000000..5efbedc --- /dev/null +++ b/module-asset/src/test/resources/application.yml @@ -0,0 +1,18 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL + username: sa + password: + driver-class-name: org.h2.Driver + + jpa: + hibernate: + ddl-auto: create-drop + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + + h2: + console: + enabled: false \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/config/SecurityConfig.java b/module-auth/src/main/java/com/ether/pms/auth/config/SecurityConfig.java index 042819f..5f55891 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/config/SecurityConfig.java +++ b/module-auth/src/main/java/com/ether/pms/auth/config/SecurityConfig.java @@ -27,6 +27,8 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -40,6 +42,11 @@ public class SecurityConfig { private final JwtTokenProvider jwtTokenProvider; + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http @@ -50,7 +57,7 @@ public class SecurityConfig { .securityContextRepository(securityContextRepository()) ) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/auth/login", "/api/auth/refresh").permitAll() + .requestMatchers("/api/auth/login", "/api/auth/logout", "/api/auth/refresh").permitAll() .requestMatchers("/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .requestMatchers("/actuator/**").permitAll() .anyRequest().authenticated() diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/DeptController.java b/module-auth/src/main/java/com/ether/pms/auth/controller/DeptController.java new file mode 100644 index 0000000..b07eb84 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/DeptController.java @@ -0,0 +1,138 @@ +package com.ether.pms.auth.controller; + +import com.ether.pms.auth.annotation.OperationLog; +import com.ether.pms.auth.controller.dto.DeptDTO; +import com.ether.pms.auth.controller.dto.DeptVO; +import com.ether.pms.auth.controller.dto.UserVO; +import com.ether.pms.auth.entity.AuditLog; +import com.ether.pms.auth.entity.Dept; +import com.ether.pms.auth.entity.User; +import com.ether.pms.auth.service.DeptService; +import com.ether.pms.common.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 部门管理控制器 + * + *

提供部门相关的RESTful API接口,包括部门树查询、创建部门、获取部门成员等功能。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@RestController +@RequestMapping("/api/auth/depts") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + /** + * 获取部门树 + */ + @GetMapping("/tree") + public ApiResponse> getDeptTree() { + List depts = deptService.getDeptTree(); + List tree = buildDeptTree(depts, null); + return ApiResponse.success(tree); + } + + /** + * 获取所有启用的部门列表 + */ + @GetMapping + public ApiResponse> getAllDepts() { + return ApiResponse.success(deptService.getActiveDepts()); + } + + /** + * 根据ID获取部门 + */ + @GetMapping("/{id}") + public ApiResponse getById(@PathVariable UUID id) { + return deptService.getById(id) + .map(ApiResponse::success) + .orElse(ApiResponse.error("部门不存在")); + } + + /** + * 创建部门 + */ + @PostMapping + @OperationLog(operation = "创建部门", module = "DEPT", action = AuditLog.ActionType.CREATE) + public ApiResponse createDept(@RequestBody @Valid DeptDTO dto) { + Dept dept = new Dept(); + dept.setDeptName(dto.getDeptName()); + dept.setDeptCode(dto.getDeptCode()); + dept.setParentId(dto.getParentId()); + dept.setDeptType(dto.getDeptType()); + dept.setDefaultRoleCode(dto.getDefaultRoleCode()); + dept.setLeaderId(dto.getLeaderId()); + dept.setSortOrder(dto.getSortOrder()); + dept.setStatus("ACTIVE"); + return ApiResponse.success(deptService.createDept(dept)); + } + + /** + * 更新部门 + */ + @PutMapping("/{id}") + @OperationLog(operation = "更新部门", module = "DEPT", action = AuditLog.ActionType.UPDATE) + public ApiResponse updateDept(@PathVariable UUID id, @RequestBody @Valid DeptDTO dto) { + Dept dept = new Dept(); + dept.setDeptName(dto.getDeptName()); + dept.setDeptCode(dto.getDeptCode()); + dept.setParentId(dto.getParentId()); + dept.setDeptType(dto.getDeptType()); + dept.setDefaultRoleCode(dto.getDefaultRoleCode()); + dept.setLeaderId(dto.getLeaderId()); + dept.setSortOrder(dto.getSortOrder()); + dept.setStatus(dto.getStatus()); + return ApiResponse.success(deptService.updateDept(id, dept)); + } + + /** + * 删除部门 + */ + @DeleteMapping("/{id}") + @OperationLog(operation = "删除部门", module = "DEPT", action = AuditLog.ActionType.DELETE) + public ApiResponse deleteDept(@PathVariable UUID id) { + deptService.deleteDept(id); + return ApiResponse.success(); + } + + /** + * 获取部门成员 + */ + @GetMapping("/{deptId}/members") + public ApiResponse> getDeptMembers(@PathVariable UUID deptId) { + List members = deptService.getDeptEmployees(deptId); + return ApiResponse.success(members.stream().map(UserVO::fromEntity).collect(Collectors.toList())); + } + + /** + * 根据部门类型查询部门 + */ + @GetMapping("/by-type/{deptType}") + public ApiResponse> getByType(@PathVariable String deptType) { + return ApiResponse.success(deptService.getByType(deptType)); + } + + private List buildDeptTree(List depts, UUID parentId) { + return depts.stream() + .filter(d -> (parentId == null && d.getParentId() == null) || + (parentId != null && parentId.equals(d.getParentId()))) + .map(d -> { + DeptVO vo = DeptVO.fromEntity(d); + vo.setChildren(buildDeptTree(depts, d.getId())); + return vo; + }) + .collect(Collectors.toList()); + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/PermissionController.java b/module-auth/src/main/java/com/ether/pms/auth/controller/PermissionController.java index cbf15d7..ebacb5b 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/controller/PermissionController.java +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/PermissionController.java @@ -10,46 +10,119 @@ import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.UUID; +/** + * 权限管理REST接口控制器 + * + *

提供权限(Permission)相关的HTTP API接口,包括权限的增删改查、类型筛选、菜单权限获取等功能。 + * 所有接口均遵循RESTful设计规范,返回统一的ApiResponse格式。

+ * + *

主要接口:

+ *
    + *
  • GET /api/auth/permissions - 查询所有权限
  • + *
  • GET /api/auth/permissions/{id} - 根据ID查询权限
  • + *
  • GET /api/auth/permissions/type/{type} - 根据类型查询权限
  • + *
  • GET /api/auth/permissions/menus - 查询所有菜单权限
  • + *
  • POST /api/auth/permissions - 创建权限
  • + *
  • PUT /api/auth/permissions/{id} - 更新权限
  • + *
  • DELETE /api/auth/permissions/{id} - 删除权限
  • + *
+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @RestController -@RequestMapping("/api/permissions") +@RequestMapping("/api/auth/permissions") @RequiredArgsConstructor public class PermissionController { - + + /** 权限业务逻辑服务 */ private final PermissionService permissionService; - + + /** + * 查询所有权限 + * + * @return 包含所有权限列表的响应 + */ @GetMapping public ResponseEntity>> findAll() { return ResponseEntity.ok(ApiResponse.success(permissionService.findAll())); } - + + /** + * 根据权限ID查询权限 + * + * @param id 权限ID + * @return 包含权限信息的响应 + */ @GetMapping("/{id}") public ResponseEntity> findById(@PathVariable UUID id) { return ResponseEntity.ok(ApiResponse.success(permissionService.findById(id))); } - + + /** + * 根据权限类型查询权限列表 + * + *

按权限类型筛选,如MENU、BUTTON、API等。

+ * + * @param type 权限类型 + * @return 包含该类型权限列表的响应 + */ @GetMapping("/type/{type}") public ResponseEntity>> findByType(@PathVariable String type) { return ResponseEntity.ok(ApiResponse.success(permissionService.findByType(type))); } - + + /** + * 查询所有菜单权限 + * + *

获取type为MENU的所有权限,通常用于前端菜单渲染。

+ * + * @return 包含菜单权限列表的响应 + */ @GetMapping("/menus") public ResponseEntity>> findMenus() { return ResponseEntity.ok(ApiResponse.success(permissionService.findMenuPermissions())); } - + + /** + * 创建新权限 + * + *

创建一个新的权限。

+ * + * @param permission 要创建的权限信息(请求体) + * @return 包含创建后权限信息的响应 + */ @PostMapping public ResponseEntity> create(@RequestBody Permission permission) { return ResponseEntity.ok(ApiResponse.success(permissionService.create(permission))); } - + + /** + * 更新权限信息 + * + *

更新指定权限的信息。

+ * + * @param id 要更新的权限ID + * @param permission 包含新数据的权限信息(请求体) + * @return 包含更新后权限信息的响应 + */ @PutMapping("/{id}") public ResponseEntity> update(@PathVariable UUID id, @RequestBody Permission permission) { return ResponseEntity.ok(ApiResponse.success(permissionService.update(id, permission))); } - + + /** + * 删除权限 + * + *

删除指定的权限。

+ * + * @param id 要删除的权限ID + * @return 空响应(表示操作成功) + */ @DeleteMapping("/{id}") public ResponseEntity> delete(@PathVariable UUID id) { permissionService.delete(id); return ResponseEntity.ok(ApiResponse.success()); } -} +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/ProjectMemberController.java b/module-auth/src/main/java/com/ether/pms/auth/controller/ProjectMemberController.java new file mode 100644 index 0000000..164cfcd --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/ProjectMemberController.java @@ -0,0 +1,146 @@ +package com.ether.pms.auth.controller; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.ether.pms.auth.annotation.OperationLog; +import com.ether.pms.auth.controller.dto.AddProjectMemberDTO; +import com.ether.pms.auth.controller.dto.PageResponse; +import com.ether.pms.auth.controller.dto.ProjectMemberVO; +import com.ether.pms.auth.controller.dto.UserVO; +import com.ether.pms.auth.entity.AuditLog; +import com.ether.pms.auth.entity.ProjectStaff; +import com.ether.pms.auth.entity.User; +import com.ether.pms.auth.service.UserManagementService; +import com.ether.pms.common.ApiResponse; + +import lombok.RequiredArgsConstructor; + +/** + * 项目成员管理控制器 + * + *

提供项目成员相关的RESTful API接口,用于管理项目成员列表、添加成员、移除成员等功能。

+ * + *

所有接口路径前缀为/api/auth/projects/{projectId}/members。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@RestController +@RequestMapping("/api/auth/projects") +@RequiredArgsConstructor +public class ProjectMemberController { + + /** 用户管理服务 */ + private final UserManagementService userManagementService; + + /** + * 查询项目成员列表 + * + *

分页返回指定项目下的所有成员信息,包含角色信息。

+ * + * @param projectId 项目唯一标识符 + * @param page 页码,从1开始 + * @param size 每页数量 + * @return 包含分页成员信息的成功响应 + */ + @GetMapping("/{projectId}/members") + public ApiResponse> getProjectMembers( + @PathVariable UUID projectId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + List staffList = userManagementService.getProjectStaffsWithRoles(projectId); + // 分页处理(支持0-based和1-based) + int pageIndex = page > 0 ? page - 1 : page; + int start = pageIndex * size; + // 边界检查 + if (start < 0) start = 0; + if (start > staffList.size()) start = staffList.size(); + int end = Math.min(start + size, staffList.size()); + List pageData = staffList.subList(start, end).stream() + .map(ProjectMemberVO::fromEntity) + .collect(Collectors.toList()); + PageResponse response = new PageResponse<>(pageData, (long) staffList.size(), pageIndex + 1, size); + return ApiResponse.success(response); + } + + /** + * 获取可添加到项目的成员列表(企业员工) + * + *

返回尚未加入该项目的所有企业员工列表,支持模糊搜索用户。

+ * + * @param projectId 项目唯一标识符 + * @param search 搜索关键字(可选),支持用户名和真实姓名模糊匹配 + * @return 包含可用成员列表的成功响应 + */ + @GetMapping("/{projectId}/available-members") + public ApiResponse> getAvailableMembers( + @PathVariable UUID projectId, + @RequestParam(required = false) String search) { + List users = userManagementService.findEnterpriseUsers(); + + // 支持模糊搜索 + if (search != null && !search.isBlank()) { + String keyword = search.toLowerCase(); + users = users.stream() + .filter(u -> u.getUsername().toLowerCase().contains(keyword) + || (u.getRealName() != null && u.getRealName().toLowerCase().contains(keyword))) + .collect(Collectors.toList()); + } + + return ApiResponse.success(users.stream() + .map(UserVO::fromEntity) + .collect(Collectors.toList())); + } + + /** + * 添加项目成员 + * + *

将指定用户添加到项目中,并分配多个角色。

+ * + * @param projectId 项目唯一标识符 + * @param dto 添加项目成员的请求数据 + * @return 成功响应 + */ + @PostMapping("/{projectId}/members") + @OperationLog(operation = "添加项目成员", module = "PROJECT_MEMBER", action = AuditLog.ActionType.CREATE) + public ApiResponse addProjectMember( + @PathVariable UUID projectId, + @RequestBody AddProjectMemberDTO dto) { + userManagementService.assignStaffToProject( + dto.getUserId(), + projectId, + dto.getStaffType(), + dto.getRoleIds()); + return ApiResponse.success(); + } + + /** + * 移除项目成员 + * + *

将指定用户从项目中移除。

+ * + * @param projectId 项目唯一标识符 + * @param userId 用户唯一标识符 + * @return 成功响应 + */ + @DeleteMapping("/{projectId}/members/{userId}") + @OperationLog(operation = "移除项目成员", module = "PROJECT_MEMBER", action = AuditLog.ActionType.DELETE) + public ApiResponse removeProjectMember( + @PathVariable UUID projectId, + @PathVariable UUID userId) { + userManagementService.removeStaffFromProject(userId, projectId); + return ApiResponse.success(); + } +} 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 ff0eb95..421eb9e 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 @@ -14,40 +14,108 @@ import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.UUID; +/** + * 角色管理REST接口控制器 + * + *

提供角色(Role)相关的HTTP API接口,包括角色的增删改查、权限分配、用户关联等功能。 + * 所有接口均遵循RESTful设计规范,返回统一的ApiResponse格式。

+ * + *

主要接口:

+ *
    + *
  • GET /api/auth/roles - 查询所有角色
  • + *
  • GET /api/auth/roles/{id} - 根据ID查询角色
  • + *
  • GET /api/auth/roles/project/{projectId} - 根据项目ID查询角色
  • + *
  • POST /api/auth/roles - 创建角色
  • + *
  • PUT /api/auth/roles/{id} - 更新角色
  • + *
  • DELETE /api/auth/roles/{id} - 删除角色
  • + *
  • POST /api/auth/roles/{id}/permissions - 为角色分配权限
  • + *
  • GET /api/auth/roles/{id}/permissions - 获取角色的权限
  • + *
  • GET /api/auth/roles/{id}/users - 获取拥有某角色的用户
  • + *
+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @RestController -@RequestMapping("/api/roles") +@RequestMapping("/api/auth/roles") @RequiredArgsConstructor public class RoleController { - + + /** 角色业务逻辑服务 */ private final RoleService roleService; - + + /** + * 查询所有角色 + * + * @return 包含所有角色列表的响应 + */ @GetMapping public ResponseEntity>> findAll() { return ResponseEntity.ok(ApiResponse.success(roleService.findAll())); } - + + /** + * 根据角色ID查询角色 + * + * @param id 角色ID + * @return 包含角色信息的响应 + */ @GetMapping("/{id}") public ResponseEntity> findById(@PathVariable UUID id) { return ResponseEntity.ok(ApiResponse.success(roleService.findById(id))); } - + + /** + * 根据项目ID查询角色列表 + * + *

获取指定项目下的所有角色。

+ * + * @param projectId 项目ID + * @return 包含该项目角色列表的响应 + */ @GetMapping("/project/{projectId}") public ResponseEntity>> findByProjectId(@PathVariable String projectId) { return ResponseEntity.ok(ApiResponse.success(roleService.findByProjectId(projectId))); } - + + /** + * 创建新角色 + * + *

创建一个新的角色,记录审计日志。

+ * + * @param role 要创建的角色信息(请求体) + * @return 包含创建后角色信息的响应 + */ @PostMapping @OperationLog(operation = "创建角色", module = "ROLE", action = AuditLog.ActionType.CREATE) public ResponseEntity> create(@RequestBody Role role) { return ResponseEntity.ok(ApiResponse.success(roleService.create(role))); } + /** + * 更新角色信息 + * + *

更新指定角色的信息,记录审计日志。

+ * + * @param id 要更新的角色ID + * @param role 包含新数据的角色信息(请求体) + * @return 包含更新后角色信息的响应 + */ @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))); } + /** + * 删除角色 + * + *

删除指定的角色,记录审计日志。

+ * + * @param id 要删除的角色ID + * @return 空响应(表示操作成功) + */ @DeleteMapping("/{id}") @OperationLog(operation = "删除角色", module = "ROLE", action = AuditLog.ActionType.DELETE) public ResponseEntity> delete(@PathVariable UUID id) { @@ -55,6 +123,15 @@ public class RoleController { return ResponseEntity.ok(ApiResponse.success()); } + /** + * 为角色分配权限 + * + *

替换角色原有的所有权限,记录审计日志。

+ * + * @param id 角色ID + * @param permissionIds 要分配的权限ID列表(请求体) + * @return 空响应(表示操作成功) + */ @PostMapping("/{id}/permissions") @OperationLog(operation = "分配权限", module = "ROLE", action = AuditLog.ActionType.ASSIGN) public ResponseEntity> assignPermissions( @@ -64,13 +141,29 @@ public class RoleController { return ResponseEntity.ok(ApiResponse.success()); } + /** + * 获取角色的所有权限 + * + *

查询指定角色关联的所有权限。

+ * + * @param id 角色ID + * @return 包含角色权限列表的响应 + */ @GetMapping("/{id}/permissions") public ResponseEntity>> getPermissions(@PathVariable UUID id) { return ResponseEntity.ok(ApiResponse.success(roleService.getPermissions(id))); } + /** + * 获取拥有某角色的所有用户 + * + *

查询所有拥有指定角色ID的用户。

+ * + * @param id 角色ID + * @return 包含用户列表的响应 + */ @GetMapping("/{id}/users") public ResponseEntity>> getUsersByRoleId(@PathVariable UUID id) { return ResponseEntity.ok(ApiResponse.success(roleService.getUsersByRoleId(id))); } -} +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/SysConfigController.java b/module-auth/src/main/java/com/ether/pms/auth/controller/SysConfigController.java index 5378fdb..388171e 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/controller/SysConfigController.java +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/SysConfigController.java @@ -13,25 +13,54 @@ import org.springframework.web.bind.annotation.*; import java.util.Map; import java.util.UUID; +/** + * 系统配置控制器 + * 提供系统配置的 RESTful API 接口 + * 包括配置的查询、更新等操作 + * + * @author Ether Team + * @since 1.0.0 + */ @RestController @RequestMapping("/api/config") @RequiredArgsConstructor public class SysConfigController { + /** + * 系统配置服务对象 + */ private final SysConfigService sysConfigService; + /** + * 获取所有配置项 + * 返回系统中所有配置的键值对 + * + * @return 配置键值对 Map + */ @GetMapping - @OperationLog(operation = "查看系统设置", module = "SYSTEM", action = AuditLog.ActionType.VIEW) public ResponseEntity>> getAllConfigs() { return ResponseEntity.ok(ApiResponse.success(sysConfigService.getAllConfigs())); } + /** + * 根据配置键获取单个配置项 + * + * @param configKey 配置键,如:property_company_name + * @return 配置项实体 + */ @GetMapping("/{configKey}") - @OperationLog(operation = "查看系统设置", module = "SYSTEM", action = AuditLog.ActionType.VIEW) public ResponseEntity> getConfig(@PathVariable String configKey) { return ResponseEntity.ok(ApiResponse.success(sysConfigService.getConfig(configKey))); } + /** + * 更新单个配置项 + * 根据配置键更新对应的配置值,并记录审计日志 + * + * @param configKey 配置键 + * @param request 更新请求对象,包含新的配置值 + * @return 更新后的配置项实体 + */ @PutMapping("/{configKey}") @OperationLog(operation = "更新系统设置", module = "SYSTEM", action = AuditLog.ActionType.UPDATE) public ResponseEntity> updateConfig( @@ -40,14 +69,28 @@ public class SysConfigController { return ResponseEntity.ok(ApiResponse.success(sysConfigService.updateConfig(configKey, request.getConfigValue()))); } + /** + * 批量更新配置项 + * 同时更新多个配置键值对,并记录审计日志 + * + * @param configs 配置键值对 Map + * @return 更新后的配置键值对 Map + */ @PutMapping @OperationLog(operation = "更新系统设置", module = "SYSTEM", action = AuditLog.ActionType.UPDATE) public ResponseEntity>> updateConfigs(@RequestBody Map configs) { return ResponseEntity.ok(ApiResponse.success(sysConfigService.updateConfigs(configs))); } + /** + * 配置更新请求对象 + * 用于接收单个配置项的更新请求 + */ @Data public static class ConfigUpdateRequest { + /** + * 新的配置值 + */ private String configValue; } } 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 10bb161..892c4e4 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 @@ -2,9 +2,11 @@ package com.ether.pms.auth.controller; import com.ether.pms.auth.annotation.OperationLog; import com.ether.pms.auth.controller.dto.UserProjectRequest; +import com.ether.pms.auth.controller.dto.UserVO; 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.UserManagementService; import com.ether.pms.auth.service.UserProjectService; import com.ether.pms.auth.service.UserService; import com.ether.pms.common.ApiResponse; @@ -15,37 +17,93 @@ import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; +/** + * 用户管理控制器 + * + *

提供用户相关的RESTful API接口,包括用户CRUD、密码管理、角色分配、项目关联等功能。

+ * + *

所有接口路径前缀为/api/auth/users,使用JSON格式进行数据交互。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @RestController -@RequestMapping("/api/users") +@RequestMapping("/api/auth/users") @RequiredArgsConstructor public class UserController { + /** 用户服务 */ private final UserService userService; + /** 用户项目关联服务 */ private final UserProjectService userProjectService; - + /** 用户管理服务 */ + private final UserManagementService userManagementService; + + /** + * 获取所有用户列表 + * + *

返回系统中所有用户的信息,包含关联的角色列表。

+ * + * @return 包含所有用户的成功响应 + */ @GetMapping public ResponseEntity>> findAll() { return ResponseEntity.ok(ApiResponse.success(userService.findAll())); } - + + /** + * 根据ID获取用户信息 + * + *

根据用户唯一标识符查询指定用户的详细信息。

+ * + * @param id 用户唯一标识符 + * @return 包含用户信息的成功响应 + */ @GetMapping("/{id}") public ResponseEntity> findById(@PathVariable UUID id) { return ResponseEntity.ok(ApiResponse.success(userService.findById(id))); } - + + /** + * 创建新用户 + * + *

创建一个新的用户账号,需要提供用户名、密码等基本信息。

+ * + * @param user 待创建的用户信息 + * @return 包含创建成功的用户信息的成功响应 + */ @PostMapping @OperationLog(operation = "创建用户", module = "USER", action = AuditLog.ActionType.CREATE) public ResponseEntity> create(@RequestBody User user) { return ResponseEntity.ok(ApiResponse.success(userService.create(user))); } + /** + * 更新用户信息 + * + *

更新指定用户的个人信息,如真实姓名、手机号、邮箱等。

+ * + * @param id 用户唯一标识符 + * @param user 包含更新信息的用户对象 + * @return 包含更新后用户信息的成功响应 + */ @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))); } + /** + * 删除用户 + * + *

根据用户ID删除指定用户账号。

+ * + * @param id 用户唯一标识符 + * @return 成功响应 + */ @DeleteMapping("/{id}") @OperationLog(operation = "删除用户", module = "USER", action = AuditLog.ActionType.DELETE) public ResponseEntity> delete(@PathVariable UUID id) { @@ -53,6 +111,15 @@ public class UserController { return ResponseEntity.ok(ApiResponse.success()); } + /** + * 修改用户密码 + * + *

用户修改自己的密码,需要提供原密码和新密码。

+ * + * @param id 用户唯一标识符 + * @param request 包含原密码和新密码的请求对象 + * @return 成功响应 + */ @PutMapping("/{id}/password") @OperationLog(operation = "修改密码", module = "USER", action = AuditLog.ActionType.UPDATE) public ResponseEntity> updatePassword( @@ -62,6 +129,15 @@ public class UserController { return ResponseEntity.ok(ApiResponse.success()); } + /** + * 分配用户角色 + * + *

为指定用户分配一个或多个角色,替换现有的角色。

+ * + * @param id 用户唯一标识符 + * @param roleIds 要分配的角色ID列表 + * @return 成功响应 + */ @PostMapping("/{id}/roles") @OperationLog(operation = "分配角色", module = "USER", action = AuditLog.ActionType.ASSIGN) public ResponseEntity> assignRoles( @@ -71,11 +147,28 @@ public class UserController { return ResponseEntity.ok(ApiResponse.success()); } + /** + * 获取用户参与的项目列表 + * + *

查询指定用户参与的所有项目及其在项目中的角色。

+ * + * @param id 用户唯一标识符 + * @return 包含用户参与项目列表的成功响应 + */ @GetMapping("/{id}/projects") public ResponseEntity>> getUserProjects(@PathVariable UUID id) { return ResponseEntity.ok(ApiResponse.success(userProjectService.getUserProjects(id))); } + /** + * 添加用户到项目 + * + *

将指定用户添加到某个项目中,并设置用户在项目中的角色。

+ * + * @param id 用户唯一标识符 + * @param request 包含项目ID和角色信息的请求对象 + * @return 成功响应 + */ @PostMapping("/{id}/projects") public ResponseEntity> addUserToProject( @PathVariable UUID id, @@ -84,6 +177,15 @@ public class UserController { return ResponseEntity.ok(ApiResponse.success()); } + /** + * 从项目中移除用户 + * + *

将指定用户从某个项目中移除,删除用户与项目的关联关系。

+ * + * @param id 用户唯一标识符 + * @param projectId 项目唯一标识符 + * @return 成功响应 + */ @DeleteMapping("/{id}/projects/{projectId}") public ResponseEntity> removeUserFromProject( @PathVariable UUID id, @@ -91,10 +193,30 @@ public class UserController { userProjectService.removeUserFromProject(id, projectId); return ResponseEntity.ok(ApiResponse.success()); } - + + /** + * 获取企业员工列表(用于项目管理添加成员) + * + *

返回所有企业员工的用户信息列表。

+ * + * @return 包含企业员工列表的成功响应 + */ + @GetMapping("/enterprise") + public ResponseEntity>> getEnterpriseUsers() { + List users = userManagementService.findEnterpriseUsers(); + return ResponseEntity.ok(ApiResponse.success(users.stream().map(UserVO::fromEntity).collect(Collectors.toList()))); + } + + /** + * 密码修改请求对象 + * + *

包含修改密码所需的原密码和新密码。

+ */ @Data public static class PasswordRequest { + /** 原密码 */ private String oldPassword; + /** 新密码 */ private String newPassword; } -} +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/AddProjectMemberDTO.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/AddProjectMemberDTO.java new file mode 100644 index 0000000..1e6e87d --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/AddProjectMemberDTO.java @@ -0,0 +1,39 @@ +package com.ether.pms.auth.controller.dto; + +import java.util.List; +import java.util.UUID; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 添加项目成员请求DTO + * + *

用于接收将用户添加到项目的请求数据。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Data +public class AddProjectMemberDTO { + + /** + * 用户ID + * 要添加到项目的用户唯一标识符 + */ + @NotNull(message = "用户ID不能为空") + private UUID userId; + + /** + * 员工类型 + * 标识员工的工作类型:SECURITY(保安)、CLEANING(保洁)、GARDEN(绿化)、MAINTENANCE(维修)、CUSTOMER_SERVICE(客服)、GENERAL(普通员工) + */ + private String staffType; + + /** + * 角色ID列表 + * 可选参数,分配的角色ID列表 + */ + private List roleIds; +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/CreateEnterpriseUserDTO.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/CreateEnterpriseUserDTO.java new file mode 100644 index 0000000..c5c59bc --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/CreateEnterpriseUserDTO.java @@ -0,0 +1,82 @@ +package com.ether.pms.auth.controller.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.time.LocalDate; +import java.util.UUID; + +/** + * 创建企业用户请求DTO + * + *

用于接收创建企业用户的请求数据,包含用户基础信息和企业扩展信息。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Data +public class CreateEnterpriseUserDTO { + + /** + * 用户名 + * 用于登录系统 + */ + @NotBlank(message = "用户名不能为空") + private String username; + + /** + * 密码 + * 用户登录密码 + */ + @NotBlank(message = "密码不能为空") + private String password; + + /** + * 真实姓名 + * 用户的真实姓名 + */ + private String realName; + + /** + * 手机号码 + * 中国大陆手机号 + */ + private String phone; + + /** + * 电子邮箱 + * 用户的邮箱地址 + */ + private String email; + + /** + * 员工工号 + * 企业内部员工编号 + */ + private String employeeNo; + + /** + * 部门ID + * 用户所属的部门标识符 + */ + private UUID deptId; + + /** + * 职位 + * 用户在企业中的职位 + */ + private String position; + + /** + * 入职日期 + * 员工加入企业的日期 + */ + private LocalDate entryDate; + + /** + * 员工类别 + * 标识员工类型:ENTERPRISE(普通员工)、MANAGEMENT(管理层) + */ + private String userCategory; +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/DeptDTO.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/DeptDTO.java new file mode 100644 index 0000000..6cb2538 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/DeptDTO.java @@ -0,0 +1,63 @@ +package com.ether.pms.auth.controller.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.util.UUID; + +/** + * 部门数据传输对象 + * + *

用于接收创建或更新部门的请求数据。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Data +public class DeptDTO { + + /** + * 部门名称 + */ + @NotBlank(message = "部门名称不能为空") + private String deptName; + + /** + * 部门编码 + */ + private String deptCode; + + /** + * 上级部门ID + */ + private UUID parentId; + + /** + * 部门类型 + * ADMIN: 行政管理 + * ENGINEERING: 工程部 + * SECURITY: 安保部 + * CS: 客服部 + * CLEANING: 保洁部 + */ + private String deptType = "ADMIN"; + + /** + * 部门默认角色编码 + * 属于该部门的员工默认拥有的系统角色 + */ + private String defaultRoleCode; + + /** + * 部门负责人ID + */ + private UUID leaderId; + + /** + * 排序序号 + */ + private Integer sortOrder; + + private String status = "ACTIVE"; +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/DeptVO.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/DeptVO.java new file mode 100644 index 0000000..5c493d1 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/DeptVO.java @@ -0,0 +1,115 @@ +package com.ether.pms.auth.controller.dto; + +import com.ether.pms.auth.entity.Dept; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * 部门视图对象 + * + *

用于返回部门信息的视图对象,包含部门基本信息和子部门列表。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Data +public class DeptVO { + + /** + * 部门唯一标识符 + */ + private UUID id; + + /** + * 上级部门ID + */ + private UUID parentId; + + /** + * 部门名称 + */ + private String deptName; + + /** + * 部门编码 + */ + private String deptCode; + + /** + * 部门类型 + */ + private String deptType; + + /** + * 部门类型描述 + */ + private String deptTypeDesc; + + /** + * 部门默认角色编码 + */ + private String defaultRoleCode; + + /** + * 部门负责人ID + */ + private UUID leaderId; + + /** + * 排序序号 + */ + private Integer sortOrder; + + /** + * 部门状态 + */ + private String status; + + /** + * 子部门列表 + */ + private List children = new ArrayList<>(); + + /** + * 将部门实体转换为视图对象 + * + * @param dept 部门实体 + * @return 部门视图对象 + */ + public static DeptVO fromEntity(Dept dept) { + DeptVO vo = new DeptVO(); + vo.setId(dept.getId()); + vo.setParentId(dept.getParentId()); + vo.setDeptName(dept.getDeptName()); + vo.setDeptCode(dept.getDeptCode()); + vo.setDeptType(dept.getDeptType()); + vo.setDeptTypeDesc(getDeptTypeDesc(dept.getDeptType())); + vo.setDefaultRoleCode(dept.getDefaultRoleCode()); + vo.setLeaderId(dept.getLeaderId()); + vo.setSortOrder(dept.getSortOrder()); + vo.setStatus(dept.getStatus()); + return vo; + } + + private static String getDeptTypeDesc(String deptType) { + if (deptType == null) { + return "行政管理"; + } + switch (deptType) { + case "ENGINEERING": + return "工程部"; + case "SECURITY": + return "安保部"; + case "CS": + return "客服部"; + case "CLEANING": + return "保洁部"; + default: + return "行政管理"; + } + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/PageResponse.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/PageResponse.java new file mode 100644 index 0000000..3478812 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/PageResponse.java @@ -0,0 +1,57 @@ +package com.ether.pms.auth.controller.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 分页响应DTO + * + *

用于返回分页数据的统一格式,包含内容列表、分页信息和总数。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PageResponse { + + /** + * 分页数据内容列表 + */ + private List content; + + /** + * 当前页数据条数 + */ + private int size; + + /** + * 当前页码 + */ + private int page; + + /** + * 数据总条数 + */ + private long totalElements; + + /** + * 构造分页响应对象 + * + * @param content 数据内容列表 + * @param totalElements 数据总条数 + * @param page 当前页码 + * @param size 每页条数 + */ + public PageResponse(List content, long totalElements, int page, int size) { + this.content = content; + this.totalElements = totalElements; + this.page = page; + this.size = size; + } +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/ProjectMemberVO.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/ProjectMemberVO.java new file mode 100644 index 0000000..03d9bf9 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/ProjectMemberVO.java @@ -0,0 +1,127 @@ +package com.ether.pms.auth.controller.dto; + +import com.ether.pms.auth.entity.ProjectStaff; +import com.ether.pms.auth.entity.ProjectStaffRole; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 项目成员视图对象 + * + *

用于返回项目成员信息,包含用户基本信息、角色信息和加入时间。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Data +public class ProjectMemberVO { + + /** + * 用户唯一标识符 + */ + private UUID id; + + /** + * 用户名 + */ + private String username; + + /** + * 真实姓名 + */ + private String realName; + + /** + * 手机号 + */ + private String phone; + + /** + * 邮箱 + */ + private String email; + + /** + * 用户类型 + */ + private String userType; + + /** + * 用户状态 + */ + private String status; + + /** + * 员工类型 + */ + private String staffType; + + /** + * 项目中的角色列表 + */ + private List roles; + + /** + * 角色名称列表(用于显示) + */ + private String roleNames; + + /** + * 加入时间 + */ + private LocalDateTime createdAt; + + /** + * 将 ProjectStaff 实体转换为视图对象 + * + * @param staff 项目员工实体 + * @return 项目成员视图对象 + */ + public static ProjectMemberVO fromEntity(ProjectStaff staff) { + ProjectMemberVO vo = new ProjectMemberVO(); + vo.setId(staff.getUser().getId()); + vo.setUsername(staff.getUser().getUsername()); + vo.setRealName(staff.getUser().getRealName()); + vo.setPhone(staff.getUser().getPhone()); + vo.setEmail(staff.getUser().getEmail()); + vo.setUserType(staff.getUser().getUserType()); + vo.setStatus(staff.getUser().getStatus() != null ? staff.getUser().getStatus().name() : null); + vo.setStaffType(staff.getStaffType()); + vo.setCreatedAt(staff.getCreatedAt()); + + // 提取角色信息:优先使用项目角色,其次使用全局角色 + List staffRoles = staff.getStaffRoles(); + List roleCodes = new ArrayList<>(); + List roleNames = new ArrayList<>(); + + if (staffRoles != null && !staffRoles.isEmpty()) { + // 使用项目角色 + for (ProjectStaffRole psr : staffRoles) { + if (psr.getRole() != null) { + roleCodes.add(psr.getRole().getCode()); + roleNames.add(psr.getRole().getName()); + } + } + } else { + // 回退到全局角色 + List userRoles = staff.getUser().getRoles(); + if (userRoles != null && !userRoles.isEmpty()) { + for (com.ether.pms.auth.entity.Role role : userRoles) { + roleCodes.add(role.getCode()); + roleNames.add(role.getName()); + } + } + } + + vo.setRoles(roleCodes); + vo.setRoleNames(String.join("、", roleNames)); + + return vo; + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserVO.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserVO.java new file mode 100644 index 0000000..e98584a --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserVO.java @@ -0,0 +1,121 @@ +package com.ether.pms.auth.controller.dto; + +import com.ether.pms.auth.entity.User; +import lombok.Data; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 用户视图对象 + * + *

用于返回用户信息的简化视图,包含用户基本信息、部门信息和角色列表。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Data +public class UserVO { + + /** + * 用户唯一标识符 + */ + private UUID id; + + /** + * 用户名 + */ + private String username; + + /** + * 真实姓名 + */ + private String realName; + + /** + * 手机号 + */ + private String phone; + + /** + * 邮箱 + */ + private String email; + + /** + * 用户类型 + */ + private String userType; + + /** + * 用户状态 + */ + private String status; + + /** + * 部门ID + */ + private UUID deptId; + + /** + * 部门名称 + */ + private String deptName; + + /** + * 角色列表 + */ + private List roles; + + /** + * 角色名称列表(逗号分隔) + */ + private String roleNames; + + /** + * 角色信息内部类 + */ + @Data + public static class RoleInfo { + private UUID id; + private String code; + private String name; + } + + /** + * 将用户实体转换为视图对象 + * + * @param user 用户实体 + * @return 用户视图对象 + */ + public static UserVO fromEntity(User user) { + UserVO vo = new UserVO(); + vo.setId(user.getId()); + vo.setUsername(user.getUsername()); + vo.setRealName(user.getRealName()); + vo.setPhone(user.getPhone()); + vo.setEmail(user.getEmail()); + vo.setUserType(user.getUserType()); + vo.setStatus(user.getStatus() != null ? user.getStatus().name() : null); + vo.setDeptId(user.getDeptId()); + + // 转换角色信息 + if (user.getRoles() != null && !user.getRoles().isEmpty()) { + List roleInfos = user.getRoles().stream() + .map(role -> { + RoleInfo info = new RoleInfo(); + info.setId(role.getId()); + info.setCode(role.getCode()); + info.setName(role.getName()); + return info; + }) + .collect(Collectors.toList()); + vo.setRoles(roleInfos); + vo.setRoleNames(roleInfos.stream().map(RoleInfo::getName).collect(Collectors.joining("、"))); + } + + return vo; + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/Dept.java b/module-auth/src/main/java/com/ether/pms/auth/entity/Dept.java new file mode 100644 index 0000000..335c054 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/Dept.java @@ -0,0 +1,131 @@ +package com.ether.pms.auth.entity; + +import jakarta.persistence.*; +import lombok.Data; +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * 部门实体类 + * + *

表示组织架构中的部门信息,包含部门名称、编码、负责人、类型等。

+ * + *

支持树形结构,通过parentId指向上级部门。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Entity +@Table(name = "dept") +@Data +public class Dept { + + /** + * 部门唯一标识符 + * 采用UUID策略自动生成 + */ + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + /** + * 上级部门ID + * 指向父部门的ID,用于构建部门树形结构,顶级部门此字段为空 + */ + private UUID parentId; + + /** + * 部门名称 + * 部门的显示名称 + */ + @Column(nullable = false, length = 100) + private String deptName; + + /** + * 部门编码 + * 部门的唯一编码,用于系统间对接 + */ + @Column(length = 50) + private String deptCode; + + /** + * 部门类型 + * 标识部门所属的业务类型 + * ADMIN: 行政管理 + * ENGINEERING: 工程部 + * SECURITY: 安保部 + * CS: 客服部 + * CLEANING: 保洁部 + */ + @Column(length = 20) + private String deptType = "ADMIN"; + + /** + * 部门默认角色编码 + * 属于该部门的员工默认拥有的系统角色 + */ + @Column(length = 50) + private String defaultRoleCode; + + /** + * 部门负责人ID + * 部门负责人的用户ID + */ + private UUID leaderId; + + /** + * 排序序号 + * 部门在同一级别中的排序顺序,数字越小越靠前 + */ + private Integer sortOrder; + + /** + * 部门状态 + * 标识部门的状态:ACTIVE-正常、DISABLED-停用 + */ + @Column(length = 20) + private String status = "ACTIVE"; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + /** + * 部门类型枚举 + */ + public enum DeptType { + ADMIN("行政管理"), + ENGINEERING("工程部"), + SECURITY("安保部"), + CS("客服部"), + CLEANING("保洁部"); + + private final String desc; + + DeptType(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/EnterpriseUser.java b/module-auth/src/main/java/com/ether/pms/auth/entity/EnterpriseUser.java new file mode 100644 index 0000000..2af111f --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/EnterpriseUser.java @@ -0,0 +1,72 @@ +package com.ether.pms.auth.entity; + +import jakarta.persistence.*; +import lombok.Data; +import java.time.LocalDate; +import java.util.UUID; + +/** + * 企业用户实体类 + * + *

表示企业类型的用户信息,包含员工的职位、入职日期等企业相关信息。

+ * + *

与User实体为一对一关系,共享主键。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Entity +@Table(name = "enterprise_user") +@Data +public class EnterpriseUser { + + /** + * 用户ID + * 主键,也是外键指向auth_user表 + */ + @Id + private UUID userId; + + /** + * 员工工号 + * 企业内部员工编号 + */ + @Column(length = 50) + private String employeeNo; + + /** + * 部门ID + * 用户所属的部门标识符 + */ + private UUID deptId; + + /** + * 职位 + * 用户在企业中的职位 + */ + @Column(length = 50) + private String position; + + /** + * 入职日期 + * 员工加入企业的日期 + */ + private LocalDate entryDate; + + /** + * 员工类别 + * 标识员工类型:ENTERPRISE(普通员工)、MANAGEMENT(管理层) + */ + @Column(length = 50) + private String userCategory; + + /** + * 关联的User实体 + * 采用一对一关系,共享主键 + */ + @OneToOne + @MapsId + @JoinColumn(name = "user_id") + private User user; +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/Permission.java b/module-auth/src/main/java/com/ether/pms/auth/entity/Permission.java index cdcdae8..5f6a5fc 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/entity/Permission.java +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/Permission.java @@ -10,47 +10,101 @@ import java.time.LocalDateTime; import java.util.List; import java.util.UUID; +/** + * 权限实体类 + * + *

用于定义系统中的权限信息,包括权限代码、名称、类型、资源路径等属性。 + * 权限是系统安全的基础单元,用于控制用户对具体功能的访问能力。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Entity @Table(name = "auth_permission") @Data public class Permission { - + + /** + * 权限唯一标识符 + * 使用UUID自动生成 + */ @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - + + /** + * 权限代码 + * 系统内部识别符,必须唯一 + * 格式:只能包含字母、数字、冒号和下划线,长度2-100位 + * 例如:system:user:create, menu:project:view + */ @NotNull(message = "权限代码不能为空") @Size(min = 2, max = 100, message = "权限代码长度必须在2-100位之间") @Pattern(regexp = "^[a-zA-Z0-9_:]+$", message = "权限代码只能包含字母、数字、冒号和下划线") @Column(unique = true, nullable = false, length = 100) private String code; - + + /** + * 权限名称 + * 用于前端展示和用户理解,长度2-100位 + */ @NotNull(message = "权限名称不能为空") @Size(min = 2, max = 100, message = "权限名称长度必须在2-100位之间") @Column(nullable = false, length = 100) private String name; - + + /** + * 权限类型 + * 用于分类权限,如:MENU(菜单)、BUTTON(按钮)、API(接口)等 + */ @Size(max = 20, message = "权限类型长度不能超过20位") @Column(length = 20) private String type; - + + /** + * 资源路径 + * 对于API类型的权限,表示对应的接口路径 + */ @Size(max = 50, message = "资源路径长度不能超过50位") @Column(length = 50) private String resource; - + + /** + * 请求方法 + * 对于API类型的权限,表示对应的HTTP方法(如GET、POST、PUT、DELETE) + */ @Size(max = 50, message = "请求方法长度不能超过50位") @Column(length = 50) private String method; - + + /** + * 权限描述 + * 对权限的详细说明,用于帮助理解权限用途,长度不超过200位 + */ @Size(max = 200, message = "权限描述长度不能超过200位") @Column(length = 200) private String description; - + + /** + * 父权限代码 + * 用于构建权限树形结构,支持层级管理 + * 顶级权限的父代码为空 + */ @Column(length = 50) private String parentCode; - + + /** + * 排序序号 + * 用于前端展示时的排序,数字越小越靠前 + */ private Integer sortOrder; - + + /** + * 权限关联的角色列表 + * 多对多关系,通过auth_role_permission关联表维护 + * JsonIgnoreProperties避免循环序列化 + */ @JsonIgnoreProperties({"permissions", "roles"}) @ManyToMany(fetch = FetchType.LAZY) @JoinTable( @@ -59,19 +113,35 @@ public class Permission { inverseJoinColumns = @JoinColumn(name = "role_id") ) private List roles; - + + /** + * 权限创建时间 + * 自动设置,记录创建时刻 + */ private LocalDateTime createdAt; - + + /** + * 权限更新时间 + * 自动设置,每次更新时自动修改 + */ private LocalDateTime updatedAt; - + + /** + * 持久化前回调 + * 自动设置创建时间和更新时间 + */ @PrePersist public void prePersist() { this.createdAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now(); } - + + /** + * 更新前回调 + * 自动设置更新时间 + */ @PreUpdate public void preUpdate() { this.updatedAt = LocalDateTime.now(); } -} +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/ProjectStaff.java b/module-auth/src/main/java/com/ether/pms/auth/entity/ProjectStaff.java new file mode 100644 index 0000000..65de930 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/ProjectStaff.java @@ -0,0 +1,115 @@ +package com.ether.pms.auth.entity; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * 项目员工实体类 + * + *

表示项目类型的员工信息,包含所属项目、员工类型、班次等信息。

+ * + *

与User实体为一对一关系,共享主键。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Entity +@Table(name = "project_staff") +@Data +public class ProjectStaff { + + /** + * 主键ID + */ + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name = "id") + private UUID id; + + /** + * 用户ID + * 外键指向auth_user表 + */ + @Column(name = "user_id", insertable = false, updatable = false) + private UUID userId; + + /** + * 所属项目ID + * 员工所属项目的唯一标识符 + */ + private UUID projectId; + + /** + * 所属部门ID + * 员工所属部门的唯一标识符,用于组织架构管理 + */ + private UUID deptId; + + /** + * 员工类型 + * 标识员工的工作类型:SECURITY(保安)、CLEANING(保洁)、GARDEN(绿化)、MAINTENANCE(维修)、CUSTOMER_SERVICE(客服)、GENERAL(普通员工) + */ + @Column(length = 50) + private String staffType; + + /** + * 班次类型 + * 标识员工的班次:DAY(白班)、NIGHT(夜班)、ROTATION(轮班) + */ + @Column(length = 20) + private String shiftType; + + /** + * 班组长ID + * 员工所属班组长的用户ID + */ + private UUID leaderId; + + /** + * 在岗状态 + * 标识员工的在岗状态:ASSIGNED(已分配)、ON_LEAVE(请假)、TRANSFERRED(已调离) + */ + @Column(length = 20) + private String assignmentStatus; + + /** + * 员工角色列表 + */ + @OneToMany(mappedBy = "staff", cascade = CascadeType.ALL, orphanRemoval = true) + private List staffRoles = new ArrayList<>(); + + /** + * 创建时间 + */ + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + /** + * 关联的User实体 + * 采用一对一关系 + */ + @OneToOne + @JoinColumn(name = "user_id") + private User user; +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/ProjectStaffRole.java b/module-auth/src/main/java/com/ether/pms/auth/entity/ProjectStaffRole.java new file mode 100644 index 0000000..7ac93d3 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/ProjectStaffRole.java @@ -0,0 +1,42 @@ +package com.ether.pms.auth.entity; + +import java.time.LocalDateTime; +import java.util.UUID; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import lombok.Data; + +@Entity +@Table(name = "project_staff_role") +@Data +public class ProjectStaffRole { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "staff_id", nullable = false) + private ProjectStaff staff; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "role_id", nullable = false) + private Role role; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/Resident.java b/module-auth/src/main/java/com/ether/pms/auth/entity/Resident.java new file mode 100644 index 0000000..28907f8 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/Resident.java @@ -0,0 +1,72 @@ +package com.ether.pms.auth.entity; + +import jakarta.persistence.*; +import lombok.Data; +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * 住户实体类 + * + *

表示住户类型的用户信息,包含身份证、认证状态等住户相关信息。

+ * + *

与User实体为一对一关系,共享主键。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Entity +@Table(name = "resident") +@Data +public class Resident { + + /** + * 用户ID + * 主键,也是外键指向auth_user表 + */ + @Id + private UUID userId; + + /** + * 身份证号码 + * 住户的身份证号,用于身份验证 + */ + @Column(length = 18) + private String idCard; + + /** + * 住户类型 + * 标识住户类型:OWNER(业主)、FAMILY(家属)、TENANT(租户) + */ + @Column(length = 20) + private String residentType; + + /** + * 认证状态 + * 标识住户的认证状态:UNVERIFIED(未认证)、PENDING(待审核)、VERIFIED(已认证)、REJECTED(已拒绝) + */ + @Column(length = 20) + private String verificationStatus; + + /** + * 认证时间 + * 记录住户认证通过的时间 + */ + private LocalDateTime verifiedAt; + + /** + * 认证人ID + * 执行认证操作的管理员用户ID + */ + private UUID verifiedBy; + + /** + * 关联的User实体 + * 采用一对一关系,共享主键 + */ + @OneToOne + @MapsId + @JoinColumn(name = "user_id") + private User user; +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/ResidentSpace.java b/module-auth/src/main/java/com/ether/pms/auth/entity/ResidentSpace.java new file mode 100644 index 0000000..3fdc583 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/ResidentSpace.java @@ -0,0 +1,71 @@ +package com.ether.pms.auth.entity; + +import jakarta.persistence.*; +import lombok.Data; +import java.time.LocalDate; +import java.util.UUID; + +/** + * 住户房屋关联实体类 + * + *

表示住户与房屋之间的关联关系,记录住户与房屋的绑定信息。

+ * + *

支持业主、家属、租户等多种关系类型,以及绑定状态管理。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Entity +@Table(name = "resident_space") +@Data +public class ResidentSpace { + + /** + * 关联记录唯一标识符 + * 采用UUID策略自动生成 + */ + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + /** + * 用户ID + * 关联的用户唯一标识符 + */ + @Column(name = "user_id", nullable = false) + private UUID userId; + + /** + * 房屋ID + * 关联的房屋唯一标识符 + */ + @Column(name = "space_id", nullable = false) + private UUID spaceId; + + /** + * 关系类型 + * 标识住户与房屋的关系:OWNER(业主)、FAMILY(家属)、TENANT(租户) + */ + @Column(length = 20) + private String relationType; + + /** + * 绑定状态 + * 标识关联的状态:PENDING(待生效)、ACTIVE(生效中)、EXPIRED(已过期)、CANCELLED(已取消) + */ + @Column(length = 20) + private String bindingStatus; + + /** + * 开始日期 + * 关联关系的生效日期 + */ + private LocalDate startDate; + + /** + * 结束日期 + * 关联关系的失效日期,永久关联则为空 + */ + private LocalDate endDate; +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/Role.java b/module-auth/src/main/java/com/ether/pms/auth/entity/Role.java index 7a5dab8..204c6d3 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/entity/Role.java +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/Role.java @@ -9,45 +9,92 @@ import java.time.LocalDateTime; import java.util.List; import java.util.UUID; +/** + * 角色实体类 + * + *

用于定义系统中的角色信息,包括角色代码、名称、描述、类型、数据范围等属性。 + * 角色与权限通过多对多关联,一个角色可以拥有多个权限。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Entity @Table(name = "auth_role") @Data public class Role { - + + /** + * 角色唯一标识符 + * 使用UUID自动生成 + */ @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - + + /** + * 角色代码 + * 用于系统内部识别,必须唯一 + * 格式:只能包含字母、数字和下划线,长度2-50位 + */ @NotNull(message = "角色代码不能为空") @Size(min = 2, max = 50, message = "角色代码长度必须在2-50位之间") @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "角色代码只能包含字母、数字和下划线") @Column(unique = true, nullable = false, length = 50) private String code; - + + /** + * 角色名称 + * 用于前端展示,长度2-50位 + */ @NotNull(message = "角色名称不能为空") @Size(min = 2, max = 50, message = "角色名称长度必须在2-50位之间") @Column(nullable = false, length = 50) private String name; - + + /** + * 角色描述 + * 对角色的详细说明,长度不超过200位 + */ @Size(max = 200, message = "角色描述长度不能超过200位") @Column(length = 200) private String description; - + + /** + * 角色类型 + * 区分系统级、项目级、部门级角色 + */ @Enumerated(EnumType.STRING) @Column(length = 20) private RoleType type; - + + /** + * 数据范围 + * 定义角色可访问的数据范围,默认为本人数据 + */ @Enumerated(EnumType.STRING) @Column(length = 20) private DataScope dataScope = DataScope.SELF; - + + /** + * 所属项目ID + * 用于项目级角色的项目归属 + */ @Column(length = 50) private String projectId; - + + /** + * 角色状态 + * 启用或禁用,默认为启用 + */ @Enumerated(EnumType.STRING) @Column(length = 20) private RoleStatus status = RoleStatus.ENABLED; - + + /** + * 角色关联的权限列表 + * 多对多关系,通过auth_role_permission关联表维护 + */ @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "auth_role_permission", @@ -55,55 +102,92 @@ public class Role { inverseJoinColumns = @JoinColumn(name = "permission_id") ) private List permissions; - + + /** + * 角色创建时间 + * 自动设置,记录创建时刻 + */ private LocalDateTime createdAt; - + + /** + * 角色更新时间 + * 自动设置,每次更新时自动修改 + */ private LocalDateTime updatedAt; - + + /** + * 持久化前回调 + * 自动设置创建时间和更新时间 + */ @PrePersist public void prePersist() { this.createdAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now(); } - + + /** + * 更新前回调 + * 自动设置更新时间 + */ @PreUpdate public void preUpdate() { this.updatedAt = LocalDateTime.now(); } - + + /** + * 角色类型枚举 + * 定义角色的层级分类 + */ public enum RoleType { + /** 系统级角色,可访问所有项目数据 */ SYSTEM("系统级"), + /** 项目级角色,仅可访问指定项目数据 */ PROJECT("项目级"), + /** 部门级角色,仅可访问本部门数据 */ DEPARTMENT("部门级"); - + private final String desc; - + RoleType(String desc) { this.desc = desc; } } - + + /** + * 数据范围枚举 + * 定义角色可查看的数据范围级别 + */ public enum DataScope { + /** 全部数据,可查看所有数据 */ ALL("全部"), + /** 本项目数据,仅可查看所属项目数据 */ PROJECT("本项目"), + /** 本部门数据,仅可查看本部门数据 */ DEPARTMENT("本部门"), + /** 本 人数据,仅可查看本人数据 */ SELF("本人"); - + private final String desc; - + DataScope(String desc) { this.desc = desc; } } - + + /** + * 角色状态枚举 + * 定义角色的启用/禁用状态 + */ public enum RoleStatus { + /** 角色已启用,可以正常使用 */ ENABLED("启用"), + /** 角色已禁用,不可使用 */ DISABLED("禁用"); - + private final String desc; - + RoleStatus(String desc) { this.desc = desc; } } -} +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/Space.java b/module-auth/src/main/java/com/ether/pms/auth/entity/Space.java new file mode 100644 index 0000000..d99e62b --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/Space.java @@ -0,0 +1,84 @@ +package com.ether.pms.auth.entity; + +import jakarta.persistence.*; +import lombok.Data; +import java.math.BigDecimal; +import java.util.UUID; + +/** + * 房屋空间实体类 + * + *

表示项目中的房屋或空间信息,包含楼栋、单元、房号等。

+ * + *

支持住宅和商业两种类型的空间管理。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Entity +@Table(name = "space") +@Data +public class Space { + + /** + * 空间唯一标识符 + * 采用UUID策略自动生成 + */ + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + /** + * 所属项目ID + * 空间所属项目的唯一标识符 + */ + private UUID projectId; + + /** + * 楼栋 + * 房屋所在的楼栋号或名称 + */ + @Column(length = 50) + private String building; + + /** + * 单元 + * 房屋所在的单元号 + */ + @Column(length = 50) + private String unit; + + /** + * 房号 + * 房屋的房间号 + */ + @Column(length = 50) + private String roomNo; + + /** + * 房屋类型 + * 标识房屋类型:RESIDENTIAL(住宅)、COMMERCIAL(商业) + */ + @Column(length = 20) + private String spaceType; + + /** + * 楼层 + * 房屋所在的楼层 + */ + private Integer floor; + + /** + * 建筑面积 + * 房屋的建筑面积,单位平方米 + */ + private BigDecimal unitArea; + + /** + * 状态 + * 标识房屋的状态:正常、空置、已售等 + */ + @Column(length = 20) + private String status; +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/SysConfig.java b/module-auth/src/main/java/com/ether/pms/auth/entity/SysConfig.java index 9adc3ab..328e08c 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/entity/SysConfig.java +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/SysConfig.java @@ -11,6 +11,13 @@ import org.hibernate.annotations.UpdateTimestamp; import java.time.LocalDateTime; import java.util.UUID; +/** + * 系统配置实体类 + * 用于存储系统的键值对配置信息,如物业企业名称等 + * + * @author Ether Team + * @since 1.0.0 + */ @Entity @Table(name = "sys_config") @Data @@ -19,23 +26,47 @@ import java.util.UUID; @Builder public class SysConfig { + /** + * 配置项唯一标识 + * 使用 UUID 自动生成 + */ @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; + /** + * 配置键 + * 唯一标识一个配置项,如:property_company_name + */ @Column(name = "config_key", nullable = false, unique = true, length = 128) private String configKey; + /** + * 配置值 + * 使用 TEXT 类型存储,支持长文本 + */ @Column(name = "config_value", columnDefinition = "TEXT") private String configValue; + /** + * 配置描述 + * 用于说明该配置项的用途 + */ @Column(name = "description", length = 256) private String description; + /** + * 创建时间 + * 自动填充,不可更新 + */ @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; + /** + * 更新时间 + * 自动填充,更新时自动修改 + */ @UpdateTimestamp @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; 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 ed12425..bf9c3cc 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 @@ -10,48 +10,120 @@ import java.time.LocalDateTime; import java.util.List; import java.util.UUID; +/** + * 用户实体类 + * + *

表示系统中的用户信息,包含用户的基本认证信息和个人资料。

+ * + *

使用JPA注解映射到auth_user表,支持基本的CRUD操作。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Entity @Table(name = "auth_user") @Data public class User { - + + /** + * 用户唯一标识符 + * 采用UUID策略自动生成 + */ @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - + + /** + * 用户名 + * 用于用户登录,唯一标识,长度3-50位,只能包含字母、数字和下划线 + */ @NotNull(message = "用户名不能为空") @Size(min = 3, max = 50, message = "用户名长度必须在3-50位之间") @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线") @Column(unique = true, nullable = false, length = 50) private String username; - + + /** + * 密码(加密存储) + * 使用BCrypt加密后的密码原文 + */ @NotNull(message = "密码不能为空") @Column(nullable = false, length = 255) private String password; - + + /** + * 密码盐值 + * 用于增强密码加密的安全性 + */ private String salt; - + + /** + * 真实姓名 + * 用户的真实姓名,用于显示 + */ @Column(length = 50) private String realName; - + + /** + * 手机号码 + * 中国大陆手机号格式,11位数字 + */ @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") @Column(length = 20) private String phone; - + + /** + * 电子邮箱 + * 用户的邮箱地址,用于通知和验证 + */ @Email(message = "邮箱格式不正确") @Column(length = 100) private String email; - + + /** + * 头像URL + * 用户头像的存储路径或URL + */ private String avatar; - + + /** + * 用户状态 + * 标识用户的当前状态:正常、锁定或禁用 + */ @Enumerated(EnumType.STRING) @Column(length = 20) private UserStatus status = UserStatus.ACTIVE; - + + /** + * 用户类型 + * 标识用户的类型:ENTERPRISE(企业用户)、PROJECT_STAFF(项目员工)、RESIDENT(住户)、CUSTOMER(客户) + */ + @Column(length = 20) + private String userType; + + /** + * 部门ID + * 用户所属的部门标识符 + */ + private UUID deptId; + + /** + * 最后登录时间 + * 记录用户最后一次成功登录的时间 + */ private LocalDateTime lastLoginTime; - + + /** + * 最后登录IP + * 记录用户最后一次成功登录的IP地址 + */ private String lastLoginIp; - + + /** + * 用户关联的角色列表 + * 采用懒加载方式获取用户的所有角色 + */ @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "auth_user_role", @@ -59,35 +131,73 @@ public class User { inverseJoinColumns = @JoinColumn(name = "role_id") ) private List roles; - + + /** + * 创建时间 + * 记录用户账号的创建时间 + */ private LocalDateTime createdAt; - + + /** + * 更新时间 + * 记录用户信息的最后修改时间 + */ private LocalDateTime updatedAt; - + + /** + * 创建人ID + * 记录创建该用户的管理员或系统ID + */ private UUID createdBy; - + + /** + * 持久化前回调 + * 在用户对象首次保存到数据库前自动设置创建时间和更新时间 + */ @PrePersist public void prePersist() { this.createdAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now(); } - + + /** + * 更新前回调 + * 在用户对象更新前自动设置更新时间 + */ @PreUpdate public void preUpdate() { this.updatedAt = LocalDateTime.now(); } - + + /** + * 用户状态枚举 + * 定义用户的三种状态:正常、锁定、禁用 + */ public enum UserStatus { + /** 正常状态 - 用户可以正常登录和使用系统 */ ACTIVE("正常"), + /** 锁定状态 - 用户被锁定,暂时无法登录 */ LOCKED("锁定"), + /** 禁用状态 - 用户被禁用,无法使用系统 */ DISABLED("禁用"); - + + /** 状态描述 */ private final String desc; - + + /** + * 构造函数 + * + * @param desc 状态描述 + */ UserStatus(String desc) { this.desc = desc; } - + + /** + * 获取状态描述 + * + * @return 状态的中文描述 + */ public String getDesc() { return desc; } diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/UserProject.java b/module-auth/src/main/java/com/ether/pms/auth/entity/UserProject.java index d5761c2..e16078c 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/entity/UserProject.java +++ b/module-auth/src/main/java/com/ether/pms/auth/entity/UserProject.java @@ -5,31 +5,66 @@ import lombok.Data; import java.time.LocalDateTime; import java.util.UUID; +/** + * 用户项目关联实体类 + * + *

表示用户与项目之间的关联关系,记录用户在特定项目中的角色和加入时间。

+ * + *

一个用户可以关联多个项目,一个项目也可以关联多个用户,形成多对多关系。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Entity @Table(name = "user_project") @Data public class UserProject { + /** + * 关联记录唯一标识符 + * 采用UUID策略自动生成 + */ @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; + /** + * 用户ID + * 关联的用户唯一标识符 + */ @Column(name = "user_id", nullable = false) private UUID userId; + /** + * 项目ID + * 关联的项目唯一标识符 + */ @Column(name = "project_id", nullable = false) private UUID projectId; + /** + * 在项目中的角色 + * 记录用户在关联项目中的角色,默认值为"member"(成员) + */ @Column(name = "role_in_project", nullable = false) private String roleInProject = "member"; + /** + * 加入时间 + * 记录用户加入项目的时间,默认为当前时间 + */ @Column(name = "joined_at", nullable = false) private LocalDateTime joinedAt = LocalDateTime.now(); + /** + * 持久化前回调 + * 在关联记录首次保存前,如果加入时间为空则设置为当前时间 + */ @PrePersist public void prePersist() { if (this.joinedAt == null) { this.joinedAt = LocalDateTime.now(); } } -} +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/DeptRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/DeptRepository.java new file mode 100644 index 0000000..ccee90e --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/DeptRepository.java @@ -0,0 +1,97 @@ +package com.ether.pms.auth.repository; + +import com.ether.pms.auth.entity.Dept; +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; + +/** + * 部门数据访问层接口 + * + *

提供部门数据的持久化操作,继承Spring Data JPA的JpaRepository接口。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Repository +public interface DeptRepository extends JpaRepository { + + /** + * 查询顶级部门列表 + * + *

查询没有上级部门的部门,即树形结构的根节点。

+ * + * @return 顶级部门列表 + */ + List findByParentIdIsNullOrderBySortOrder(); + + /** + * 根据父部门ID查询子部门列表 + * + * @param parentId 父部门唯一标识符 + * @return 该父部门下的所有子部门列表 + */ + List findByParentIdOrderBySortOrder(UUID parentId); + + /** + * 根据部门编码查询部门 + * + * @param deptCode 部门编码 + * @return 包含部门的Optional对象 + */ + Optional findByDeptCode(String deptCode); + + /** + * 根据部门类型查询部门列表 + * + * @param deptType 部门类型 + * @return 该类型的所有部门列表 + */ + List findByDeptTypeOrderBySortOrder(String deptType); + + /** + * 查询所有启用的部门 + * + * @return 所有状态为ACTIVE的部门 + */ + List findByStatusOrderBySortOrder(String status); + + /** + * 根据部门负责人查询部门 + * + * @param leaderId 负责人用户ID + * @return 该负责人管理的部门列表 + */ + List findByLeaderId(UUID leaderId); + + /** + * 查询所有部门(用于树形结构构建) + * + * @return 所有部门列表 + */ + @Query("SELECT d FROM Dept d ORDER BY d.sortOrder") + List findAllForTree(); + + /** + * 检查部门编码是否存在 + * + * @param deptCode 部门编码 + * @return 存在返回true + */ + boolean existsByDeptCode(String deptCode); + + /** + * 根据ID查询部门及其默认角色 + * + * @param id 部门ID + * @return 部门的默认角色编码 + */ + @Query("SELECT d.defaultRoleCode FROM Dept d WHERE d.id = :id") + String findDefaultRoleCodeById(@Param("id") UUID id); +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/EnterpriseUserRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/EnterpriseUserRepository.java new file mode 100644 index 0000000..d7e4595 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/EnterpriseUserRepository.java @@ -0,0 +1,38 @@ +package com.ether.pms.auth.repository; + +import com.ether.pms.auth.entity.EnterpriseUser; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * 企业用户数据访问层接口 + * + *

提供企业用户数据的持久化操作,继承Spring Data JPA的JpaRepository接口。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Repository +public interface EnterpriseUserRepository extends JpaRepository { + + /** + * 根据用户ID查询企业用户 + * + * @param userId 用户唯一标识符 + * @return 包含企业用户的Optional对象 + */ + Optional findByUserId(UUID userId); + + /** + * 根据部门ID查询企业用户列表 + * + * @param deptId 部门唯一标识符 + * @return 该部门下的所有企业用户列表 + */ + List findByDeptId(UUID deptId); +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/PermissionRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/PermissionRepository.java index 265a817..7490d48 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/repository/PermissionRepository.java +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/PermissionRepository.java @@ -6,12 +6,46 @@ import org.springframework.stereotype.Repository; import java.util.List; import java.util.UUID; +/** + * 权限数据访问层接口 + * + *

提供权限(Permission)实体的数据库操作方法,继承Spring Data JPA的JpaRepository。 + * 支持按权限类型、父权限代码等条件查询。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Repository public interface PermissionRepository extends JpaRepository { - + + /** + * 根据权限类型查询权限列表 + * + *

按权限类型筛选,如MENU(菜单)、BUTTON(按钮)、API(接口)等。

+ * + * @param type 权限类型 + * @return 该类型的所有权限列表 + */ List findByType(String type); - + + /** + * 根据父权限代码查询子权限列表 + * + *

用于构建权限树形结构,查找属于某个父权限的所有子权限。

+ * + * @param parentCode 父权限代码 + * @return 该父权限下的所有子权限列表 + */ List findByParentCode(String parentCode); - + + /** + * 检查权限代码是否存在 + * + *

用于创建权限时的唯一性校验。

+ * + * @param code 权限代码 + * @return 存在返回true,不存在返回false + */ boolean existsByCode(String code); -} +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/ProjectStaffRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/ProjectStaffRepository.java new file mode 100644 index 0000000..6eeaac4 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/ProjectStaffRepository.java @@ -0,0 +1,86 @@ +package com.ether.pms.auth.repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import com.ether.pms.auth.entity.ProjectStaff; + +/** + * 项目员工数据访问层接口 + * + *

提供项目员工数据的持久化操作,继承Spring Data JPA的JpaRepository接口。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Repository +public interface ProjectStaffRepository extends JpaRepository { + + /** + * 根据用户ID查询项目员工 + * + * @param userId 用户唯一标识符 + * @return 包含项目员工的Optional对象 + */ + Optional findByUserId(UUID userId); + + /** + * 根据用户ID查询所有项目员工记录 + * + * @param userId 用户唯一标识符 + * @return 该用户的所有项目员工记录列表 + */ + List findAllByUserId(UUID userId); + + /** + * 根据项目ID查询所有项目员工 + * + * @param projectId 项目唯一标识符 + * @return 该项目下的所有员工列表 + */ + List findByProjectId(UUID projectId); + + /** + * 根据项目ID查询所有项目员工(包含角色信息) + * + * @param projectId 项目唯一标识符 + * @return 该项目下的所有员工列表(包含角色信息) + */ + @Query("SELECT DISTINCT ps FROM ProjectStaff ps " + + "LEFT JOIN FETCH ps.staffRoles sr " + + "LEFT JOIN FETCH sr.role " + + "LEFT JOIN FETCH ps.user " + + "WHERE ps.projectId = :projectId") + List findByProjectIdWithRoles(UUID projectId); + + /** + * 根据用户ID和项目ID删除项目员工关联 + * + * @param userId 用户唯一标识符 + * @param projectId 项目唯一标识符 + */ + void deleteByUserIdAndProjectId(UUID projectId, UUID userId); + + /** + * 根据用户ID和项目ID查询项目员工 + * + * @param userId 用户唯一标识符 + * @param projectId 项目唯一标识符 + * @return 包含项目员工的Optional对象 + */ + Optional findByUserIdAndProjectId(UUID userId, UUID projectId); + + /** + * 根据项目ID统计成员数量 + * + * @param projectId 项目唯一标识符 + * @return 该项目的成员数量 + */ + long countByProjectId(UUID projectId); +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/ProjectStaffRoleRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/ProjectStaffRoleRepository.java new file mode 100644 index 0000000..7a244ee --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/ProjectStaffRoleRepository.java @@ -0,0 +1,37 @@ +package com.ether.pms.auth.repository; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.ether.pms.auth.entity.ProjectStaffRole; + +@Repository +public interface ProjectStaffRoleRepository extends JpaRepository { + + /** + * 根据员工ID查询所有角色关联 + * + * @param staffId 员工ID + * @return 角色关联列表 + */ + List findByStaff_Id(UUID staffId); + + /** + * 根据员工ID删除所有角色关联 + * + * @param staffId 员工ID + */ + void deleteByStaff_Id(UUID staffId); + + /** + * 检查是否存在指定员工和角色的关联 + * + * @param staffId 员工ID + * @param roleId 角色ID + * @return 是否存在 + */ + boolean existsByStaff_IdAndRole_Id(UUID staffId, UUID roleId); +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/ResidentRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/ResidentRepository.java new file mode 100644 index 0000000..d068764 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/ResidentRepository.java @@ -0,0 +1,38 @@ +package com.ether.pms.auth.repository; + +import com.ether.pms.auth.entity.Resident; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * 住户数据访问层接口 + * + *

提供住户数据的持久化操作,继承Spring Data JPA的JpaRepository接口。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Repository +public interface ResidentRepository extends JpaRepository { + + /** + * 根据用户ID查询住户 + * + * @param userId 用户唯一标识符 + * @return 包含住户的Optional对象 + */ + Optional findByUserId(UUID userId); + + /** + * 根据认证状态查询住户列表 + * + * @param status 认证状态 + * @return 该状态下的所有住户列表 + */ + List findByVerificationStatus(String status); +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/ResidentSpaceRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/ResidentSpaceRepository.java new file mode 100644 index 0000000..afa46ee --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/ResidentSpaceRepository.java @@ -0,0 +1,47 @@ +package com.ether.pms.auth.repository; + +import com.ether.pms.auth.entity.ResidentSpace; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * 住户房屋关联数据访问层接口 + * + *

提供住户房屋关联数据的持久化操作,继承Spring Data JPA的JpaRepository接口。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Repository +public interface ResidentSpaceRepository extends JpaRepository { + + /** + * 根据用户ID查询所有住户房屋关联 + * + * @param userId 用户唯一标识符 + * @return 该用户的所有住户房屋关联列表 + */ + List findByUserId(UUID userId); + + /** + * 根据房屋ID查询所有住户房屋关联 + * + * @param spaceId 房屋唯一标识符 + * @return 该房屋的所有住户关联列表 + */ + List findBySpaceId(UUID spaceId); + + /** + * 根据用户ID和房屋ID查询住户房屋关联 + * + * @param userId 用户唯一标识符 + * @param spaceId 房屋唯一标识符 + * @return 包含住户房屋关联的Optional对象 + */ + Optional findByUserIdAndSpaceId(UUID userId, UUID spaceId); +} 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 bafb3c9..0de9cb5 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 @@ -8,17 +8,68 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +/** + * 角色数据访问层接口 + * + *

提供角色(Role)实体的数据库操作方法,继承Spring Data JPA的JpaRepository。 + * 支持按角色代码、项目ID、类型等条件查询,以及带权限信息的复杂查询。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Repository public interface RoleRepository extends JpaRepository { + /** + * 根据角色代码查询角色 + * + *

角色代码在系统中唯一,用于精确查找特定角色。

+ * + * @param code 角色代码 + * @return 角色对象(如果存在) + */ Optional findByCode(String code); + /** + * 检查角色代码是否存在 + * + *

用于创建角色时的唯一性校验。

+ * + * @param code 角色代码 + * @return 存在返回true,不存在返回false + */ boolean existsByCode(String code); + /** + * 根据项目ID查询角色列表 + * + *

获取指定项目下的所有角色,通常用于项目级别的角色筛选。

+ * + * @param projectId 项目ID + * @return 该项目下的角色列表 + */ List findByProjectId(String projectId); + /** + * 根据角色类型查询角色列表 + * + *

按角色类型(如系统级、项目级、部门级)筛选角色。

+ * + * @param type 角色类型枚举 + * @return 该类型的所有角色列表 + */ List findByType(Role.RoleType type); + /** + * 根据角色ID查询角色及其关联的权限信息 + * + *

使用EntityGraph eagerly加载permissions关联数据, + * 避免N+1查询问题,提升查询性能。

+ * + * @param id 角色ID + * @return 包含权限信息的角色对象(如果存在) + */ @EntityGraph(attributePaths = {"permissions"}) Optional findWithPermissionsById(UUID id); -} +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/SpaceRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/SpaceRepository.java new file mode 100644 index 0000000..cd340cb --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/SpaceRepository.java @@ -0,0 +1,41 @@ +package com.ether.pms.auth.repository; + +import com.ether.pms.auth.entity.Space; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * 房屋空间数据访问层接口 + * + *

提供房屋空间数据的持久化操作,继承Spring Data JPA的JpaRepository接口。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Repository +public interface SpaceRepository extends JpaRepository { + + /** + * 根据项目ID查询所有房屋空间 + * + * @param projectId 项目唯一标识符 + * @return 该项目下的所有房屋空间列表 + */ + List findByProjectId(UUID projectId); + + /** + * 根据项目ID、楼栋、单元和房号查询房屋空间 + * + * @param projectId 项目唯一标识符 + * @param building 楼栋 + * @param unit 单元 + * @param roomNo 房号 + * @return 包含房屋空间的Optional对象 + */ + Optional findByProjectIdAndBuildingAndUnitAndRoomNo(UUID projectId, String building, String unit, String roomNo); +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/SysConfigRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/SysConfigRepository.java index c187e29..0edb8a1 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/repository/SysConfigRepository.java +++ b/module-auth/src/main/java/com/ether/pms/auth/repository/SysConfigRepository.java @@ -7,8 +7,21 @@ import org.springframework.stereotype.Repository; import java.util.Optional; import java.util.UUID; +/** + * 系统配置数据访问层 + * 提供对 sys_config 表的 CRUD 操作 + * + * @author Ether Team + * @since 1.0.0 + */ @Repository public interface SysConfigRepository extends JpaRepository { + /** + * 根据配置键查询配置项 + * + * @param configKey 配置键,如:property_company_name + * @return 配置项 Optional 包装 + */ Optional findByConfigKey(String configKey); } 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 bca1003..ee73234 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 @@ -10,19 +10,85 @@ import org.springframework.stereotype.Repository; import java.util.List; import java.util.UUID; +/** + * 用户项目关联数据访问层接口 + * + *

提供用户与项目关联关系的持久化操作,继承Spring Data JPA的JpaRepository接口。

+ * + *

支持用户项目关系查询、分页、批量操作等功能。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Repository public interface UserProjectRepository extends JpaRepository { + /** + * 根据用户ID查询该用户关联的所有项目 + * + *

返回指定用户参与的所有项目关联记录列表。

+ * + * @param userId 用户唯一标识符 + * @return 该用户关联的所有项目列表 + */ List findByUserId(UUID userId); + /** + * 根据项目ID查询该项目的所有关联用户 + * + *

返回参与指定项目的所有用户关联记录列表。

+ * + * @param projectId 项目唯一标识符 + * @return 该项目关联的所有用户列表 + */ List findByProjectId(UUID projectId); + /** + * 根据项目ID分页查询该项目的所有关联用户 + * + *

支持分页展示项目成员列表。

+ * + * @param projectId 项目唯一标识符 + * @param pageable 分页参数,包含页码和每页大小 + * @return 分页的用户项目关联列表 + */ Page findByProjectId(UUID projectId, Pageable pageable); + /** + * 根据用户ID查询该用户关联的所有项目ID + * + *

仅返回项目ID列表,用于快速判断用户参与的项目。

+ * + * @param userId 用户唯一标识符 + * @return 该用户关联的所有项目ID列表 + */ @Query("SELECT up.projectId FROM UserProject up WHERE up.userId = :userId") List findProjectIdsByUserId(@Param("userId") UUID userId); + /** + * 检查用户与项目的关联是否存在 + * + *

用于判断用户是否参与了指定项目。

+ * + * @param userId 用户唯一标识符 + * @param projectId 项目唯一标识符 + * @return 存在返回true,不存在返回false + */ boolean existsByUserIdAndProjectId(UUID userId, UUID projectId); + /** + * 删除用户与项目的关联 + * + *

根据用户ID和项目ID删除关联记录,用于用户退出项目或移除项目成员。

+ * + * @param userId 用户唯一标识符 + * @param projectId 项目唯一标识符 + */ void deleteByUserIdAndProjectId(UUID userId, UUID projectId); -} + + /** + * 统计项目下的成员数量 + */ + long countByProjectId(UUID projectId); +} \ No newline at end of file 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 d831b40..ec68913 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 @@ -9,24 +9,121 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +/** + * 用户数据访问层接口 + * + *

提供用户数据的持久化操作,继承Spring Data JPA的JpaRepository接口。

+ * + *

包含用户查询、角色关联查询等数据库操作方法。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Repository public interface UserRepository extends JpaRepository { + /** + * 根据用户名查询用户 + * + *

最基础的按用户名查询方法,返回Optional包装的用户对象。

+ * + * @param username 用户名 + * @return 包含用户的Optional对象,如果不存在则为空 + */ Optional findByUsername(String username); + /** + * 根据用户名查询用户及其关联的角色 + * + *

使用LEFT JOIN FETCH预加载用户的角色信息,避免N+1查询问题。

+ * + * @param username 用户名 + * @return 包含用户及其角色的Optional对象 + */ @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username") Optional findByUsernameWithRoles(@Param("username") String username); + /** + * 查询所有用户及其关联的角色 + * + *

一次性加载所有用户及其角色信息,适用于管理后台用户列表。

+ * + * @return 包含所有用户及其角色的列表 + */ @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles") List findAllWithRoles(); + /** + * 根据ID查询用户及其关联的角色 + * + *

使用LEFT JOIN FETCH预加载用户的角色信息。

+ * + * @param id 用户唯一标识符 + * @return 包含用户及其角色的Optional对象 + */ @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.id = :id") Optional findByIdWithRoles(@Param("id") UUID id); + /** + * 根据角色ID查询所有拥有该角色的用户 + * + *

用于查询具有特定角色的所有用户列表。

+ * + * @param roleId 角色唯一标识符 + * @return 拥有该角色的所有用户列表 + */ @Query("SELECT u FROM User u JOIN u.roles r WHERE r.id = :roleId") List findByRoleId(@Param("roleId") UUID roleId); + /** + * 检查用户名是否存在 + * + *

用于用户注册时验证用户名唯一性。

+ * + * @param username 用户名 + * @return 存在返回true,不存在返回false + */ boolean existsByUsername(String username); + /** + * 检查手机号是否存在 + * + *

用于用户注册或更新时验证手机号唯一性。

+ * + * @param phone 手机号码 + * @return 存在返回true,不存在返回false + */ boolean existsByPhone(String phone); -} + + /** + * 根据用户类型查询用户列表 + * + *

用于查询特定类型的用户,如ENTERPRISE、PROJECT_STAFF等。

+ * + * @param userType 用户类型 + * @return 该类型的所有用户列表 + */ + List findByUserType(String userType); + + /** + * 根据部门ID查询用户列表 + * + *

用于查询属于特定部门的所有用户。

+ * + * @param deptId 部门唯一标识符 + * @return 该部门下的所有用户列表 + */ + List findByDeptId(UUID deptId); + + /** + * 根据项目ID查询项目员工 + * + *

通过关联ProjectStaff表查询属于指定项目的所有员工用户。

+ * + * @param projectId 项目唯一标识符 + * @return该项目下的所有员工用户列表 + */ + @Query("SELECT u FROM User u JOIN ProjectStaff ps ON u.id = ps.userId WHERE ps.projectId = :projectId") + List findProjectStaffsByProjectId(@Param("projectId") UUID projectId); +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/DeptService.java b/module-auth/src/main/java/com/ether/pms/auth/service/DeptService.java new file mode 100644 index 0000000..f36b34f --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/service/DeptService.java @@ -0,0 +1,154 @@ +package com.ether.pms.auth.service; + +import com.ether.pms.auth.entity.Dept; +import com.ether.pms.auth.entity.User; +import com.ether.pms.auth.repository.DeptRepository; +import com.ether.pms.auth.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * 部门服务层 + * + *

提供部门相关的业务逻辑处理,包括部门树查询、部门CRUD、部门员工查询等功能。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Service +@RequiredArgsConstructor +public class DeptService { + + private final DeptRepository deptRepository; + private final UserRepository userRepository; + + /** + * 获取部门树 + * + *

返回所有部门的列表,支持前端构建树形结构。

+ * + * @return 所有部门的列表 + */ + public List getDeptTree() { + return deptRepository.findAllForTree(); + } + + /** + * 获取所有启用的部门列表 + * + * @return 启用的部门列表 + */ + public List getActiveDepts() { + return deptRepository.findByStatusOrderBySortOrder("ACTIVE"); + } + + /** + * 根据ID获取部门 + * + * @param id 部门ID + * @return 部门对象 + */ + public Optional getById(UUID id) { + return deptRepository.findById(id); + } + + /** + * 获取部门及下属员工 + * + *

根据部门ID查询该部门下的所有员工用户。

+ * + * @param deptId 部门唯一标识符 + * @return 该部门下的所有员工用户列表 + */ + public List getDeptEmployees(UUID deptId) { + return userRepository.findByDeptId(deptId); + } + + /** + * 创建部门 + * + * @param dept 待创建的部门对象 + * @return 创建成功的部门对象 + */ + @Transactional + public Dept createDept(Dept dept) { + if (dept.getSortOrder() == null) { + dept.setSortOrder(0); + } + if (dept.getStatus() == null) { + dept.setStatus("ACTIVE"); + } + if (dept.getDeptType() == null) { + dept.setDeptType("ADMIN"); + } + return deptRepository.save(dept); + } + + /** + * 更新部门 + * + * @param id 部门ID + * @param dept 更新后的部门信息 + * @return 更新后的部门对象 + */ + @Transactional + public Dept updateDept(UUID id, Dept dept) { + Dept existing = deptRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("部门不存在: " + id)); + + existing.setDeptName(dept.getDeptName()); + existing.setDeptCode(dept.getDeptCode()); + existing.setParentId(dept.getParentId()); + existing.setDeptType(dept.getDeptType()); + existing.setDefaultRoleCode(dept.getDefaultRoleCode()); + existing.setLeaderId(dept.getLeaderId()); + existing.setSortOrder(dept.getSortOrder()); + existing.setStatus(dept.getStatus()); + + return deptRepository.save(existing); + } + + /** + * 删除部门 + * + * @param id 部门ID + */ + @Transactional + public void deleteDept(UUID id) { + Dept dept = deptRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("部门不存在: " + id)); + + List children = deptRepository.findByParentIdOrderBySortOrder(id); + if (!children.isEmpty()) { + throw new IllegalStateException("请先删除子部门"); + } + + deptRepository.delete(dept); + } + + /** + * 根据部门类型查询部门 + * + * @param deptType 部门类型 + * @return 该类型的部门列表 + */ + public List getByType(String deptType) { + return deptRepository.findByDeptTypeOrderBySortOrder(deptType); + } + + /** + * 检查部门编码是否存在 + * + * @param deptCode 部门编码 + * @return 存在返回true + */ + public boolean existsByCode(String deptCode) { + return deptRepository.existsByDeptCode(deptCode); + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/LoginService.java b/module-auth/src/main/java/com/ether/pms/auth/service/LoginService.java index c356095..bdf0eee 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/service/LoginService.java +++ b/module-auth/src/main/java/com/ether/pms/auth/service/LoginService.java @@ -1,6 +1,8 @@ package com.ether.pms.auth.service; +import com.ether.pms.auth.entity.ProjectStaff; import com.ether.pms.auth.entity.User; +import com.ether.pms.auth.repository.ProjectStaffRepository; import com.ether.pms.auth.repository.UserRepository; import com.ether.pms.auth.util.JwtTokenProvider; import com.ether.pms.common.BusinessException; @@ -9,57 +11,72 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class LoginService { - + private final UserRepository userRepository; + private final ProjectStaffRepository projectStaffRepository; private final JwtTokenProvider jwtTokenProvider; private final PasswordService passwordService; private final LoginAttemptService loginAttemptService; - + @Transactional public Map login(String username, String password, String ip) { if (loginAttemptService.isLockedOut(username)) { throw new BusinessException(ErrorCode.AUTH_002); } - + User user = userRepository.findByUsernameWithRoles(username).orElse(null); - + if (user == null || !passwordService.matches(password, user.getPassword())) { if (user != null) { loginAttemptService.recordFailure(username); } throw new BusinessException(ErrorCode.AUTH_001); } - + if (user.getStatus() == User.UserStatus.LOCKED) { throw new BusinessException(ErrorCode.AUTH_002); } if (user.getStatus() == User.UserStatus.DISABLED) { throw new BusinessException(ErrorCode.AUTH_003); } - + loginAttemptService.recordSuccess(username); - - Map claims = new HashMap<>(); + + Set allRoles = new HashSet<>(); if (user.getRoles() != null) { - claims.put("roles", user.getRoles().stream() - .map(r -> r.getCode()) - .toList()); + allRoles.addAll(user.getRoles().stream() + .map(r -> r.getCode()) + .toList()); } - + + List projectStaffs = projectStaffRepository.findAllByUserId(user.getId()); + for (ProjectStaff staff : projectStaffs) { + if (staff.getStaffRoles() != null) { + allRoles.addAll(staff.getStaffRoles().stream() + .filter(sr -> sr.getRole() != null) + .map(sr -> sr.getRole().getCode()) + .toList()); + } + } + + Map claims = new HashMap<>(); + claims.put("roles", new ArrayList<>(allRoles)); + String token = jwtTokenProvider.generateToken(user.getId(), user.getUsername(), claims); - + Map result = new HashMap<>(); result.put("token", token); result.put("userId", user.getId()); result.put("username", user.getUsername()); result.put("realName", user.getRealName()); - + result.put("roles", new ArrayList<>(allRoles)); + return result; } } diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/PasswordService.java b/module-auth/src/main/java/com/ether/pms/auth/service/PasswordService.java index d2fa31e..1d12240 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/service/PasswordService.java +++ b/module-auth/src/main/java/com/ether/pms/auth/service/PasswordService.java @@ -44,15 +44,15 @@ public class PasswordService { throw new IllegalArgumentException("密码长度必须在" + minLength + "-" + maxLength + "位之间"); } - if (requireUppercase && !containsAny(password, 'A', 'Z')) { + if (requireUppercase && !Pattern.compile("[A-Z]").matcher(password).find()) { throw new IllegalArgumentException("密码必须包含大写字母"); } - - if (requireLowercase && !containsAny(password, 'a', 'z')) { + + if (requireLowercase && !Pattern.compile("[a-z]").matcher(password).find()) { throw new IllegalArgumentException("密码必须包含小写字母"); } - if (requireDigit && !containsAny(password, '0', '9')) { + if (requireDigit && !Pattern.compile("[0-9]").matcher(password).find()) { throw new IllegalArgumentException("密码必须包含数字"); } diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/PermissionService.java b/module-auth/src/main/java/com/ether/pms/auth/service/PermissionService.java index 1588010..e5b71c2 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/service/PermissionService.java +++ b/module-auth/src/main/java/com/ether/pms/auth/service/PermissionService.java @@ -11,33 +11,91 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.UUID; +/** + * 权限业务逻辑服务类 + * + *

提供权限(Permission)相关的业务操作,包括权限的增删改查、类型筛选、菜单权限获取等功能。 + * 所有写操作均支持事务管理,确保数据一致性。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Service @RequiredArgsConstructor public class PermissionService { - + + /** 权限数据访问仓库 */ private final PermissionRepository permissionRepository; - + + /** + * 查询所有权限 + * + * @return 所有权限的列表 + */ public List findAll() { return permissionRepository.findAll(); } - + + /** + * 根据权限ID查询权限 + * + *

如果权限不存在,抛出PERMISSION_002业务异常。

+ * + * @param id 权限ID + * @return 权限对象 + * @throws BusinessException 如果权限不存在 + */ public Permission findById(UUID id) { return permissionRepository.findById(id) .orElseThrow(() -> new BusinessException(ErrorCode.PERMISSION_002)); } - + + /** + * 根据权限类型查询权限列表 + * + *

按权限类型筛选,如MENU、BUTTON、API等。

+ * + * @param type 权限类型 + * @return 该类型的所有权限列表 + */ public List findByType(String type) { return permissionRepository.findByType(type); } - + + /** + * 根据父权限代码查询子权限列表 + * + *

用于获取某个父权限下的所有子权限,构建权限树。

+ * + * @param parentCode 父权限代码 + * @return 该父权限下的所有子权限列表 + */ public List findByParentCode(String parentCode) { return permissionRepository.findByParentCode(parentCode); } - + + /** + * 查询所有菜单类型权限 + * + *

获取type为MENU的所有权限,通常用于前端菜单渲染。

+ * + * @return 所有菜单权限列表 + */ public List findMenuPermissions() { return permissionRepository.findByType("MENU"); } - + + /** + * 创建新权限 + * + *

如果权限代码已存在,抛出PERMISSION_001业务异常。 + * 创建时会自动设置创建时间和更新时间。

+ * + * @param permission 要创建的权限对象 + * @return 创建后的权限对象(包含数据库生成的主键) + * @throws BusinessException 如果权限代码已存在 + */ @Transactional public Permission create(Permission permission) { if (permissionRepository.existsByCode(permission.getCode())) { @@ -45,11 +103,22 @@ public class PermissionService { } return permissionRepository.save(permission); } - + + /** + * 更新权限信息 + * + *

仅更新传入参数中非空的字段,保持其他字段不变。 + * 如果权限不存在,抛出PERMISSION_002业务异常。

+ * + * @param id 要更新的权限ID + * @param permission 包含新数据的权限对象 + * @return 更新后的权限对象 + * @throws BusinessException 如果权限不存在 + */ @Transactional public Permission update(UUID id, Permission permission) { Permission existing = findById(id); - + if (permission.getName() != null) { existing.setName(permission.getName()); } @@ -71,12 +140,19 @@ public class PermissionService { if (permission.getSortOrder() != null) { existing.setSortOrder(permission.getSortOrder()); } - + return permissionRepository.save(existing); } - + + /** + * 删除权限 + * + *

根据权限ID删除权限记录。

+ * + * @param id 要删除的权限ID + */ @Transactional public void delete(UUID id) { permissionRepository.deleteById(id); } -} +} \ No newline at end of file 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 e8e2bb0..71f39ea 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 @@ -15,32 +15,88 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.UUID; +/** + * 角色业务逻辑服务类 + * + *

提供角色(Role)相关的业务操作,包括角色的增删改查、权限分配、用户关联等功能。 + * 所有操作均支持事务管理,确保数据一致性。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Service @RequiredArgsConstructor public class RoleService { - + + /** 角色数据访问仓库 */ private final RoleRepository roleRepository; + + /** 权限数据访问仓库 */ private final PermissionRepository permissionRepository; + + /** 用户数据访问仓库 */ private final UserRepository userRepository; - + + /** + * 查询所有角色 + * + * @return 所有角色的列表 + */ public List findAll() { return roleRepository.findAll(); } - + + /** + * 根据角色ID查询角色 + * + *

如果角色不存在,抛出ROLE_002业务异常。

+ * + * @param id 角色ID + * @return 角色对象 + * @throws BusinessException 如果角色不存在 + */ public Role findById(UUID id) { return roleRepository.findById(id) .orElseThrow(() -> new BusinessException(ErrorCode.ROLE_002)); } - + + /** + * 根据角色代码查询角色 + * + *

如果角色不存在,抛出ROLE_002业务异常。

+ * + * @param code 角色代码 + * @return 角色对象 + * @throws BusinessException 如果角色不存在 + */ public Role findByCode(String code) { return roleRepository.findByCode(code) .orElseThrow(() -> new BusinessException(ErrorCode.ROLE_002)); } - + + /** + * 根据项目ID查询角色列表 + * + *

获取指定项目下的所有角色。

+ * + * @param projectId 项目ID + * @return 该项目下的角色列表 + */ public List findByProjectId(String projectId) { return roleRepository.findByProjectId(projectId); } - + + /** + * 创建新角色 + * + *

如果角色代码已存在,抛出ROLE_001业务异常。 + * 创建时会自动设置创建时间和更新时间。

+ * + * @param role 要创建的角色对象 + * @return 创建后的角色对象(包含数据库生成的主键) + * @throws BusinessException 如果角色代码已存在 + */ @Transactional public Role create(Role role) { if (roleRepository.existsByCode(role.getCode())) { @@ -48,11 +104,22 @@ public class RoleService { } return roleRepository.save(role); } - + + /** + * 更新角色信息 + * + *

仅更新传入参数中非空的字段,保持其他字段不变。 + * 如果角色不存在,抛出ROLE_002业务异常。

+ * + * @param id 要更新的角色ID + * @param role 包含新数据的角色对象 + * @return 更新后的角色对象 + * @throws BusinessException 如果角色不存在 + */ @Transactional public Role update(UUID id, Role role) { Role existing = findById(id); - + if (role.getName() != null) { existing.setName(role.getName()); } @@ -71,10 +138,20 @@ public class RoleService { if (role.getStatus() != null) { existing.setStatus(role.getStatus()); } - + return roleRepository.save(existing); } - + + /** + * 为角色分配权限 + * + *

替换角色原有的所有权限为新分配的权限列表。 + * 如果角色或权限不存在,抛出相应业务异常。

+ * + * @param roleId 角色ID + * @param permissionIds 要分配的权限ID列表 + * @throws BusinessException 如果角色或权限不存在 + */ @Transactional public void assignPermissions(UUID roleId, List permissionIds) { Role role = findById(roleId); @@ -82,19 +159,61 @@ public class RoleService { role.setPermissions(permissions); roleRepository.save(role); } - + + /** + * 删除角色 + * + *

根据角色ID删除角色记录。

+ * + * @param id 要删除的角色ID + */ @Transactional public void delete(UUID id) { roleRepository.deleteById(id); } + /** + * 获取角色的所有权限 + * + *

使用EntityGraph eagerly加载权限信息,避免N+1查询。 + * 如果角色不存在,抛出ROLE_002业务异常。

+ * + * @param roleId 角色ID + * @return 该角色关联的所有权限列表 + * @throws BusinessException 如果角色不存在 + */ public List getPermissions(UUID roleId) { Role role = roleRepository.findWithPermissionsById(roleId) .orElseThrow(() -> new BusinessException(ErrorCode.ROLE_002)); return role.getPermissions(); } + /** + * 根据角色ID查询关联的用户列表 + * + *

获取拥有该角色的所有用户。

+ * + * @param roleId 角色ID + * @return 拥有该角色的用户列表 + */ public List getUsersByRoleId(UUID roleId) { return userRepository.findByRoleId(roleId); } -} + + /** + * 为用户分配角色 + * + *

根据角色编码查找角色并分配给用户。

+ * + * @param userId 用户ID + * @param roleCode 角色编码 + */ + @Transactional + public void assignRoleToUser(UUID userId, String roleCode) { + Role role = findByCode(roleCode); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_003)); + user.getRoles().add(role); + userRepository.save(user); + } +} \ No newline at end of file diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/SysConfigService.java b/module-auth/src/main/java/com/ether/pms/auth/service/SysConfigService.java index b33ebba..5b3680a 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/service/SysConfigService.java +++ b/module-auth/src/main/java/com/ether/pms/auth/service/SysConfigService.java @@ -11,12 +11,28 @@ import java.util.List; import java.util.Map; import java.util.UUID; +/** + * 系统配置服务层 + * 提供系统配置的查询、更新等业务逻辑处理 + * + * @author Ether Team + * @since 1.0.0 + */ @Service @RequiredArgsConstructor public class SysConfigService { + /** + * 系统配置数据访问对象 + */ private final SysConfigRepository sysConfigRepository; + /** + * 获取所有配置项 + * 将配置项列表转换为键值对 Map 返回 + * + * @return 配置键值对 Map,key 为 configKey,value 为 configValue + */ public Map getAllConfigs() { List configs = sysConfigRepository.findAll(); Map result = new HashMap<>(); @@ -24,10 +40,25 @@ public class SysConfigService { return result; } + /** + * 根据配置键获取单个配置项 + * + * @param configKey 配置键,如:property_company_name + * @return 配置项实体,不存在时返回 null + */ public SysConfig getConfig(String configKey) { return sysConfigRepository.findByConfigKey(configKey).orElse(null); } + /** + * 更新单个配置项 + * 根据配置键查找并更新对应的配置值 + * + * @param configKey 配置键 + * @param configValue 新的配置值 + * @return 更新后的配置项实体 + * @throws RuntimeException 当配置项不存在时抛出 + */ @Transactional public SysConfig updateConfig(String configKey, String configValue) { SysConfig config = sysConfigRepository.findByConfigKey(configKey) @@ -36,6 +67,14 @@ public class SysConfigService { return sysConfigRepository.save(config); } + /** + * 批量更新配置项 + * 同时更新多个配置键值对 + * + * @param configs 配置键值对 Map + * @return 更新后的配置键值对 Map + * @throws RuntimeException 当某个配置项不存在时抛出 + */ @Transactional public Map updateConfigs(Map configs) { Map result = new HashMap<>(); diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/UserManagementService.java b/module-auth/src/main/java/com/ether/pms/auth/service/UserManagementService.java new file mode 100644 index 0000000..c8bc3c8 --- /dev/null +++ b/module-auth/src/main/java/com/ether/pms/auth/service/UserManagementService.java @@ -0,0 +1,273 @@ +package com.ether.pms.auth.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.hibernate.Hibernate; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.ether.pms.auth.controller.dto.CreateEnterpriseUserDTO; +import com.ether.pms.auth.entity.EnterpriseUser; +import com.ether.pms.auth.entity.ProjectStaff; +import com.ether.pms.auth.entity.ProjectStaffRole; +import com.ether.pms.auth.entity.Role; +import com.ether.pms.auth.entity.User; +import com.ether.pms.auth.repository.DeptRepository; +import com.ether.pms.auth.repository.EnterpriseUserRepository; +import com.ether.pms.auth.repository.ProjectStaffRepository; +import com.ether.pms.auth.repository.ProjectStaffRoleRepository; +import com.ether.pms.auth.repository.ResidentRepository; +import com.ether.pms.auth.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +/** + * 用户管理服务层 + * + *

提供用户管理的核心业务逻辑,包括企业员工管理、项目员工分配等功能。

+ * + *

作为用户管理的核心服务,供其他模块调用。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ +@Service +@RequiredArgsConstructor +public class UserManagementService { + + /** 用户数据访问接口 */ + private final UserRepository userRepository; + + /** 企业用户数据访问接口 */ + private final EnterpriseUserRepository enterpriseUserRepository; + + /** 项目员工数据访问接口 */ + private final ProjectStaffRepository projectStaffRepository; + + /** 项目员工角色数据访问接口 */ + private final ProjectStaffRoleRepository projectStaffRoleRepository; + + /** 角色服务 */ + private final RoleService roleService; + + /** 部门数据访问接口 */ + private final DeptRepository deptRepository; + + /** 住户数据访问接口 */ + private final ResidentRepository residentRepository; + + /** 密码加密器 */ + private final PasswordEncoder passwordEncoder; + + /** + * 查询所有企业员工 + * + *

返回用户类型为ENTERPRISE的所有用户列表。

+ * + * @return 所有企业用户的列表 + */ + public List findEnterpriseUsers() { + return userRepository.findByUserType("ENTERPRISE"); + } + + /** + * 查询项目员工 + * + *

根据项目ID查询该项目下的所有员工。

+ * + * @param projectId 项目唯一标识符 + * @return 该项目下的所有员工列表 + */ + public List findProjectStaffs(UUID projectId) { + return userRepository.findProjectStaffsByProjectId(projectId); + } + + /** + * 创建企业员工 + * + *

创建基础用户信息和企业员工扩展信息,包括工号、部门、职位等。 + * 自动分配该部门的默认角色到用户。

+ * + * @param dto 创建企业用户的请求数据 + * @return 创建成功的企业用户对象 + */ + @Transactional + public User createEnterpriseUser(CreateEnterpriseUserDTO dto) { + // 1. 创建基础用户 + User user = new User(); + user.setUsername(dto.getUsername()); + user.setPassword(passwordEncoder.encode(dto.getPassword())); + user.setRealName(dto.getRealName()); + user.setPhone(dto.getPhone()); + user.setEmail(dto.getEmail()); + user.setUserType("ENTERPRISE"); + user.setDeptId(dto.getDeptId()); + user.setStatus(User.UserStatus.ACTIVE); + user = userRepository.save(user); + + // 2. 创建企业员工扩展信息 + EnterpriseUser eu = new EnterpriseUser(); + eu.setUser(user); + eu.setEmployeeNo(dto.getEmployeeNo()); + eu.setDeptId(dto.getDeptId()); + eu.setPosition(dto.getPosition()); + eu.setEntryDate(dto.getEntryDate()); + eu.setUserCategory(dto.getUserCategory()); + enterpriseUserRepository.save(eu); + + // 3. 自动分配部门的默认角色 + if (dto.getDeptId() != null) { + String defaultRoleCode = deptRepository.findDefaultRoleCodeById(dto.getDeptId()); + if (defaultRoleCode != null && !defaultRoleCode.isEmpty()) { + Role defaultRole = roleService.findByCode(defaultRoleCode); + if (defaultRole != null) { + assignRoleToUser(user.getId(), defaultRole.getId()); + } + } + } + + return user; + } + + /** + * 为用户分配角色 + * + * @param userId 用户ID + * @param roleId 角色ID + */ + @Transactional + public void assignRoleToUser(UUID userId, UUID roleId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId)); + Role role = roleService.findById(roleId); + if (role == null) { + throw new IllegalArgumentException("Role not found: " + roleId); + } + + if (user.getRoles() == null) { + throw new IllegalStateException("User roles collection not initialized"); + } + + boolean hasRole = user.getRoles().stream() + .anyMatch(r -> r.getId().equals(roleId)); + if (!hasRole) { + user.getRoles().add(role); + userRepository.save(user); + } + } + + /** + * 分配员工到项目 + * + *

将用户分配到指定项目,设定员工类型和分配状态,支持多角色分配。

+ * + * @param userId 用户唯一标识符 + * @param projectId 项目唯一标识符 + * @param staffType 员工类型 + * @param roleIds 角色ID列表 + * @return 创建的项目员工关联记录 + */ + @Transactional + public ProjectStaff assignStaffToProject(UUID userId, UUID projectId, String staffType, List roleIds) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId)); + + // 检查用户是否已经是该项目成员 + ProjectStaff staff = projectStaffRepository.findByUserIdAndProjectId(userId, projectId) + .orElse(null); + + if (staff == null) { + // 不存在,创建新的项目员工记录 + staff = new ProjectStaff(); + staff.setUser(user); + staff.setProjectId(projectId); + staff.setStaffType(staffType != null ? staffType : "GENERAL"); + staff.setAssignmentStatus("ASSIGNED"); + staff.setCreatedAt(LocalDateTime.now()); + staff.setUpdatedAt(LocalDateTime.now()); + staff = projectStaffRepository.save(staff); + } else { + // 用户已在项目中,更新员工类型(只有当staffType变化时才更新) + if (staffType != null && !staffType.equals(staff.getStaffType())) { + staff.setStaffType(staffType); + staff.setUpdatedAt(LocalDateTime.now()); + staff = projectStaffRepository.save(staff); + } + } + + // 角色关联:先删除旧角色,再添加新角色(覆盖模式) + if (roleIds != null) { + // 删除该员工在该项目下的所有旧角色关联 + List existingRoles = staff.getStaffRoles(); + if (existingRoles != null && !existingRoles.isEmpty()) { + projectStaffRoleRepository.deleteAll(existingRoles); + staff.getStaffRoles().clear(); + } + // 添加新角色 + if (!roleIds.isEmpty()) { + for (UUID roleId : roleIds) { + Role role = roleService.findById(roleId); + if (role != null) { + ProjectStaffRole staffRole = new ProjectStaffRole(); + staffRole.setStaff(staff); + staffRole.setRole(role); + projectStaffRoleRepository.save(staffRole); + staff.getStaffRoles().add(staffRole); + } + } + } + } + + return staff; + } + + /** + * 查询项目成员 + * + *

根据项目ID查询该项目下的所有成员用户。

+ * + * @param projectId 项目唯一标识符 + * @return 该项目下的所有成员用户列表 + */ + public List getProjectMembers(UUID projectId) { + return userRepository.findProjectStaffsByProjectId(projectId); + } + + /** + * 查询项目员工列表(包含角色信息) + * + *

根据项目ID查询该项目下的所有员工信息,包含角色信息。

+ * + * @param projectId 项目唯一标识符 + * @return 该项目下的所有员工列表 + */ + public List getProjectStaffsWithRoles(UUID projectId) { + List staffs = projectStaffRepository.findByProjectIdWithRoles(projectId); + for (ProjectStaff staff : staffs) { + Hibernate.initialize(staff.getUser().getRoles()); + } + return staffs; + } + + /** + * 从项目移除员工 + * + *

将用户从指定项目中移除,删除项目员工关联关系。

+ * + * @param userId 用户唯一标识符 + * @param projectId 项目唯一标识符 + */ + @Transactional + public void removeStaffFromProject(UUID userId, UUID projectId) { + Optional staff = projectStaffRepository.findByUserIdAndProjectId(userId, projectId); + staff.ifPresent(s -> { + // 级联删除角色关联(通过 orphanRemoval) + projectStaffRepository.delete(s); + }); + } +} diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/UserProjectService.java b/module-auth/src/main/java/com/ether/pms/auth/service/UserProjectService.java index 18a2d82..579dee2 100644 --- a/module-auth/src/main/java/com/ether/pms/auth/service/UserProjectService.java +++ b/module-auth/src/main/java/com/ether/pms/auth/service/UserProjectService.java @@ -8,20 +8,52 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.UUID; +/** + * 用户项目关联服务层 + * + *

提供用户与项目关联关系的业务逻辑处理,包括关联查询、添加、移除等功能。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Service @RequiredArgsConstructor public class UserProjectService { + /** 用户项目关联数据访问接口 */ private final UserProjectRepository userProjectRepository; + /** + * 获取用户参与的所有项目 + * + *

根据用户ID查询该用户参与的所有项目关联记录。

getUserProjects(UUID userId) { return userProjectRepository.findByUserId(userId); } + /** + * 获取用户参与的所有项目ID + * + *

根据用户ID查询该用户参与的所有项目ID列表,用于权限验证等场景。

getUserProjectIds(UUID userId) { return userProjectRepository.findProjectIdsByUserId(userId); } + /** + * 添加用户到项目 + * + *

创建用户与项目之间的关联关系。如果已存在关联则不重复创建。

删除用户与项目之间的关联关系。

判断用户与项目之间是否存在关联关系。

提供用户相关的业务逻辑处理,包括用户CRUD、密码管理、角色分配等功能。

+ * + *

所有涉及数据修改的操作均添加事务注解保证数据一致性。

+ * + * @author Ether开发团队 + * @version 1.0.0 + * @since 2024-01-01 + */ @Service @RequiredArgsConstructor public class UserService { - + + /** 用户数据访问接口 */ private final UserRepository userRepository; + /** 角色数据访问接口 */ private final RoleRepository roleRepository; + /** 密码服务接口 */ private final PasswordService passwordService; - + + /** + * 查询所有用户 + * + *

返回所有用户及其关联的角色信息。

+ * + * @return 所有用户的列表 + */ public List findAll() { return userRepository.findAllWithRoles(); } - + + /** + * 根据ID查询用户 + * + *

根据用户唯一标识符查询用户信息。

+ * + * @param id 用户唯一标识符 + * @return 用户对象 + * @throws BusinessException 如果用户不存在,抛出业务异常 + */ public User findById(UUID id) { return userRepository.findById(id) .orElseThrow(() -> new BusinessException(ErrorCode.USER_003)); } - + + /** + * 根据用户名查询用户 + * + *

根据用户名精确查询用户信息,用于登录验证等场景。

+ * + * @param username 用户名 + * @return 用户对象 + * @throws BusinessException 如果用户不存在,抛出业务异常 + */ public User findByUsername(String username) { return userRepository.findByUsername(username) .orElseThrow(() -> new BusinessException(ErrorCode.USER_003)); } - + + /** + * 创建新用户 + * + *

创建新用户并对密码进行加密存储,同时验证用户名和手机号的唯一性。

+ * + * @param user 待创建的用户对象 + * @return 创建成功的用户对象(包含数据库生成的ID) + * @throws BusinessException 如果用户名已存在、手机号已存在或密码不符合要求 + */ @Transactional public User create(User user) { if (userRepository.existsByUsername(user.getUsername())) { @@ -44,26 +92,36 @@ public class UserService { if (user.getPhone() != null && userRepository.existsByPhone(user.getPhone())) { throw new BusinessException(ErrorCode.USER_002); } - + try { passwordService.validatePassword(user.getPassword()); } catch (IllegalArgumentException e) { throw new BusinessException(ErrorCode.BAD_REQUEST, e.getMessage()); } - + if (passwordService.isPasswordWeak(user.getPassword())) { throw new BusinessException(ErrorCode.BAD_REQUEST, "密码太弱,请使用更复杂的密码"); } - + user.setPassword(passwordService.encode(user.getPassword())); - + return userRepository.save(user); } - + + /** + * 更新用户信息 + * + *

根据用户ID更新用户信息,仅更新非空字段。

+ * + * @param id 用户唯一标识符 + * @param user 包含更新信息的用户对象 + * @return 更新后的用户对象 + * @throws BusinessException 如果用户不存在或手机号已被其他用户使用 + */ @Transactional public User update(UUID id, User user) { User existing = findById(id); - + if (user.getRealName() != null) { existing.setRealName(user.getRealName()); } @@ -82,50 +140,75 @@ public class UserService { if (user.getStatus() != null) { existing.setStatus(user.getStatus()); } - + return userRepository.save(existing); } - + + /** + * 修改用户密码 + * + *

用户主动修改密码,需验证原密码正确性,新密码需符合强度要求。

管理员重置用户密码,新密码需符合强度要求。

为用户分配一个或多个角色,替换用户现有的所有角色。

roleIds) { User user = findById(userId); @@ -133,16 +216,29 @@ public class UserService { user.setRoles(roles); userRepository.save(user); } - + + /** + * 删除用户 + * + *

根据用户ID删除用户记录。

记录用户登录的时间戳和IP地址,用于安全审计。

(List.of(staffRole))); + + // When + ProjectMemberVO vo = ProjectMemberVO.fromEntity(staff); + + // Then + assertNotNull(vo); + assertEquals(userId, vo.getId()); + assertEquals("testuser", vo.getUsername()); + assertEquals("测试用户", vo.getRealName()); + assertEquals("13800138000", vo.getPhone()); + assertEquals("test@example.com", vo.getEmail()); + assertEquals("ENTERPRISE", vo.getUserType()); + assertEquals("ACTIVE", vo.getStatus()); + assertEquals("SECURITY", vo.getStaffType()); + assertEquals(createdAt, vo.getCreatedAt()); + assertEquals(List.of("PROJECT_ADMIN"), vo.getRoles()); + assertEquals("项目管理员", vo.getRoleNames()); + } + + @Test + void fromEntity_shouldHandleMultipleRoles() { + // Given + UUID userId = UUID.randomUUID(); + + User user = new User(); + user.setId(userId); + user.setUsername("testuser"); + user.setRealName("测试用户"); + user.setStatus(User.UserStatus.ACTIVE); + + Role role1 = new Role(); + role1.setId(UUID.randomUUID()); + role1.setCode("PROJECT_ADMIN"); + role1.setName("项目管理员"); + + Role role2 = new Role(); + role2.setId(UUID.randomUUID()); + role2.setCode("SECURITY_LEAD"); + role2.setName("保安队长"); + + ProjectStaffRole staffRole1 = new ProjectStaffRole(); + staffRole1.setRole(role1); + + ProjectStaffRole staffRole2 = new ProjectStaffRole(); + staffRole2.setRole(role2); + + ProjectStaff staff = new ProjectStaff(); + staff.setUser(user); + staff.setStaffRoles(new ArrayList<>(List.of(staffRole1, staffRole2))); + + // When + ProjectMemberVO vo = ProjectMemberVO.fromEntity(staff); + + // Then + assertNotNull(vo); + assertEquals(2, vo.getRoles().size()); + assertTrue(vo.getRoles().contains("PROJECT_ADMIN")); + assertTrue(vo.getRoles().contains("SECURITY_LEAD")); + assertEquals("项目管理员、保安队长", vo.getRoleNames()); + } + + @Test + void fromEntity_shouldHandleEmptyRoles() { + // Given + UUID userId = UUID.randomUUID(); + + User user = new User(); + user.setId(userId); + user.setUsername("testuser"); + user.setRealName("测试用户"); + user.setStatus(User.UserStatus.ACTIVE); + + ProjectStaff staff = new ProjectStaff(); + staff.setUser(user); + staff.setStaffRoles(new ArrayList<>()); + + // When + ProjectMemberVO vo = ProjectMemberVO.fromEntity(staff); + + // Then + assertNotNull(vo); + assertTrue(vo.getRoles().isEmpty()); + assertEquals("", vo.getRoleNames()); + } + + @Test + void fromEntity_shouldHandleNullRoles() { + // Given + UUID userId = UUID.randomUUID(); + + User user = new User(); + user.setId(userId); + user.setUsername("testuser"); + user.setRealName("测试用户"); + user.setStatus(User.UserStatus.ACTIVE); + + ProjectStaff staff = new ProjectStaff(); + staff.setUser(user); + staff.setStaffRoles(null); + + // When + ProjectMemberVO vo = ProjectMemberVO.fromEntity(staff); + + // Then + assertNotNull(vo); + assertTrue(vo.getRoles().isEmpty()); + assertEquals("", vo.getRoleNames()); + } + + @Test + void fromEntity_shouldUseGlobalRoles_whenProjectRolesEmpty() { + // Given + UUID userId = UUID.randomUUID(); + + User user = new User(); + user.setId(userId); + user.setUsername("testuser"); + user.setRealName("测试用户"); + user.setStatus(User.UserStatus.ACTIVE); + + Role globalRole = new Role(); + globalRole.setId(UUID.randomUUID()); + globalRole.setCode("SYS_ADMIN"); + globalRole.setName("系统管理员"); + user.setRoles(new ArrayList<>(List.of(globalRole))); + + ProjectStaff staff = new ProjectStaff(); + staff.setUser(user); + staff.setStaffRoles(new ArrayList<>()); // 空的项目角色 + + // When + ProjectMemberVO vo = ProjectMemberVO.fromEntity(staff); + + // Then + assertNotNull(vo); + assertEquals(List.of("SYS_ADMIN"), vo.getRoles()); + assertEquals("系统管理员", vo.getRoleNames()); + } + + @Test + void fromEntity_shouldHandleNullUserFields() { + // Given + UUID userId = UUID.randomUUID(); + + User user = new User(); + user.setId(userId); + user.setUsername("testuser"); + // 其他字段为null + + ProjectStaff staff = new ProjectStaff(); + staff.setUser(user); + staff.setStaffRoles(new ArrayList<>()); + + // When + ProjectMemberVO vo = ProjectMemberVO.fromEntity(staff); + + // Then + assertNotNull(vo); + assertEquals(userId, vo.getId()); + assertEquals("testuser", vo.getUsername()); + assertNull(vo.getRealName()); + assertNull(vo.getPhone()); + assertNull(vo.getEmail()); + assertNull(vo.getUserType()); + // status 字段在 User 实体中有默认值 ACTIVE + assertEquals("ACTIVE", vo.getStatus()); + } + + @Test + void fromEntity_shouldHandleNullRoleInStaffRole() { + // Given + UUID userId = UUID.randomUUID(); + + User user = new User(); + user.setId(userId); + user.setUsername("testuser"); + user.setRealName("测试用户"); + user.setStatus(User.UserStatus.ACTIVE); + + ProjectStaffRole staffRoleWithNullRole = new ProjectStaffRole(); + staffRoleWithNullRole.setRole(null); + + ProjectStaff staff = new ProjectStaff(); + staff.setUser(user); + staff.setStaffRoles(new ArrayList<>(List.of(staffRoleWithNullRole))); + + // When + ProjectMemberVO vo = ProjectMemberVO.fromEntity(staff); + + // Then + assertNotNull(vo); + assertTrue(vo.getRoles().isEmpty()); + assertEquals("", vo.getRoleNames()); + } +} diff --git a/module-auth/src/test/java/com/ether/pms/auth/service/UserManagementServiceTest.java b/module-auth/src/test/java/com/ether/pms/auth/service/UserManagementServiceTest.java new file mode 100644 index 0000000..2c9f057 --- /dev/null +++ b/module-auth/src/test/java/com/ether/pms/auth/service/UserManagementServiceTest.java @@ -0,0 +1,427 @@ +package com.ether.pms.auth.service; + +import com.ether.pms.auth.entity.ProjectStaff; +import com.ether.pms.auth.entity.ProjectStaffRole; +import com.ether.pms.auth.entity.Role; +import com.ether.pms.auth.entity.User; +import com.ether.pms.auth.repository.ProjectStaffRepository; +import com.ether.pms.auth.repository.ProjectStaffRoleRepository; +import com.ether.pms.auth.repository.UserRepository; +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.security.crypto.password.PasswordEncoder; + +import java.util.ArrayList; +import java.util.List; +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.ArgumentMatchers.anyList; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class UserManagementServiceTest { + + @Mock + private UserRepository userRepository; + + @Mock + private ProjectStaffRepository projectStaffRepository; + + @Mock + private ProjectStaffRoleRepository projectStaffRoleRepository; + + @Mock + private RoleService roleService; + + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private UserManagementService userManagementService; + + @Test + void assignStaffToProject_shouldCreateNewStaff_whenUserNotInProject() { + // Given + UUID userId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + UUID roleId = UUID.randomUUID(); + String staffType = "GENERAL"; + List roleIds = List.of(roleId); + + User mockUser = new User(); + mockUser.setId(userId); + mockUser.setUsername("testuser"); + + Role mockRole = new Role(); + mockRole.setId(roleId); + mockRole.setCode("PROJECT_ADMIN"); + mockRole.setName("项目管理员"); + + ProjectStaff savedStaff = new ProjectStaff(); + savedStaff.setId(UUID.randomUUID()); + savedStaff.setUser(mockUser); + savedStaff.setProjectId(projectId); + savedStaff.setStaffType(staffType); + savedStaff.setStaffRoles(new ArrayList<>()); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(projectStaffRepository.findByUserIdAndProjectId(userId, projectId)).thenReturn(Optional.empty()); + when(projectStaffRepository.save(any(ProjectStaff.class))).thenReturn(savedStaff); + when(roleService.findById(roleId)).thenReturn(mockRole); + when(projectStaffRoleRepository.save(any(ProjectStaffRole.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // When + ProjectStaff result = userManagementService.assignStaffToProject(userId, projectId, staffType, roleIds); + + // Then + assertNotNull(result); + assertEquals(mockUser, result.getUser()); + assertEquals(projectId, result.getProjectId()); + assertEquals(staffType, result.getStaffType()); + verify(projectStaffRepository).save(any(ProjectStaff.class)); + verify(projectStaffRoleRepository).save(any(ProjectStaffRole.class)); + } + + @Test + void assignStaffToProject_shouldReuseExistingStaffAndUpdateStaffType_whenUserAlreadyInProject() { + // Given + UUID userId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + UUID roleId = UUID.randomUUID(); + String staffType = "SECURITY"; + List roleIds = List.of(roleId); + + User mockUser = new User(); + mockUser.setId(userId); + mockUser.setUsername("testuser"); + + Role mockRole = new Role(); + mockRole.setId(roleId); + mockRole.setCode("PROJECT_ADMIN"); + mockRole.setName("项目管理员"); + + ProjectStaff existingStaff = new ProjectStaff(); + existingStaff.setId(UUID.randomUUID()); + existingStaff.setUser(mockUser); + existingStaff.setProjectId(projectId); + existingStaff.setStaffType("GENERAL"); + existingStaff.setStaffRoles(new ArrayList<>()); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(projectStaffRepository.findByUserIdAndProjectId(userId, projectId)).thenReturn(Optional.of(existingStaff)); + when(projectStaffRepository.save(any(ProjectStaff.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(roleService.findById(roleId)).thenReturn(mockRole); + when(projectStaffRoleRepository.save(any(ProjectStaffRole.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // When + ProjectStaff result = userManagementService.assignStaffToProject(userId, projectId, staffType, roleIds); + + // Then + assertNotNull(result); + assertEquals(existingStaff.getId(), result.getId()); + assertEquals("SECURITY", result.getStaffType()); + verify(projectStaffRepository, times(1)).save(any(ProjectStaff.class)); + verify(projectStaffRoleRepository).save(any(ProjectStaffRole.class)); + } + + @Test + void assignStaffToProject_shouldClearRolesAndAddNewRoles_whenRoleIdsProvided() { + // Given: 用户已有一个角色 + UUID userId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + UUID newRoleId = UUID.randomUUID(); + String staffType = "GENERAL"; + List newRoleIds = List.of(newRoleId); + + User mockUser = new User(); + mockUser.setId(userId); + + Role newRole = new Role(); + newRole.setId(newRoleId); + newRole.setCode("NEW_ROLE"); + + ProjectStaff existingStaff = new ProjectStaff(); + existingStaff.setId(UUID.randomUUID()); + existingStaff.setUser(mockUser); + existingStaff.setProjectId(projectId); + existingStaff.setStaffType("GENERAL"); // staffType相同,不会触发staff.save + existingStaff.setStaffRoles(new ArrayList<>()); // 空的,不需要删除 + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(projectStaffRepository.findByUserIdAndProjectId(userId, projectId)).thenReturn(Optional.of(existingStaff)); + when(roleService.findById(newRoleId)).thenReturn(newRole); + when(projectStaffRoleRepository.save(any(ProjectStaffRole.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // When + ProjectStaff result = userManagementService.assignStaffToProject(userId, projectId, staffType, newRoleIds); + + // Then + assertNotNull(result); + verify(projectStaffRoleRepository, never()).deleteAll(anyList()); // 没有旧角色,不需要删除 + verify(projectStaffRoleRepository, times(1)).save(any(ProjectStaffRole.class)); + assertEquals(1, result.getStaffRoles().size()); + } + + @Test + void assignStaffToProject_shouldThrowException_whenUserNotFound() { + // Given + UUID userId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + List roleIds = List.of(); + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + // Then + assertThrows(IllegalArgumentException.class, () -> + userManagementService.assignStaffToProject(userId, projectId, "GENERAL", roleIds) + ); + } + + @Test + void assignStaffToProject_shouldHandleMultipleRoles() { + // Given + UUID userId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + UUID roleId1 = UUID.randomUUID(); + UUID roleId2 = UUID.randomUUID(); + String staffType = "GENERAL"; + List roleIds = List.of(roleId1, roleId2); + + User mockUser = new User(); + mockUser.setId(userId); + + Role mockRole1 = new Role(); + mockRole1.setId(roleId1); + mockRole1.setCode("PROJECT_ADMIN"); + mockRole1.setName("项目管理员"); + + Role mockRole2 = new Role(); + mockRole2.setId(roleId2); + mockRole2.setCode("SECURITY_LEAD"); + mockRole2.setName("保安队长"); + + ProjectStaff savedStaff = new ProjectStaff(); + savedStaff.setId(UUID.randomUUID()); + savedStaff.setUser(mockUser); + savedStaff.setProjectId(projectId); + savedStaff.setStaffRoles(new ArrayList<>()); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(projectStaffRepository.findByUserIdAndProjectId(userId, projectId)).thenReturn(Optional.empty()); + when(projectStaffRepository.save(any(ProjectStaff.class))).thenReturn(savedStaff); + when(roleService.findById(roleId1)).thenReturn(mockRole1); + when(roleService.findById(roleId2)).thenReturn(mockRole2); + when(projectStaffRoleRepository.save(any(ProjectStaffRole.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // When + ProjectStaff result = userManagementService.assignStaffToProject(userId, projectId, staffType, roleIds); + + // Then + assertNotNull(result); + // 应该添加两个角色 + verify(projectStaffRoleRepository, times(2)).save(any(ProjectStaffRole.class)); + } + + @Test + void assignStaffToProject_shouldHandleEmptyRoleIds() { + // Given + UUID userId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + String staffType = "GENERAL"; + List roleIds = List.of(); + + User mockUser = new User(); + mockUser.setId(userId); + + ProjectStaff savedStaff = new ProjectStaff(); + savedStaff.setId(UUID.randomUUID()); + savedStaff.setUser(mockUser); + savedStaff.setProjectId(projectId); + savedStaff.setStaffRoles(new ArrayList<>()); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(projectStaffRepository.findByUserIdAndProjectId(userId, projectId)).thenReturn(Optional.empty()); + when(projectStaffRepository.save(any(ProjectStaff.class))).thenReturn(savedStaff); + + // When + ProjectStaff result = userManagementService.assignStaffToProject(userId, projectId, staffType, roleIds); + + // Then + assertNotNull(result); + // 不应该添加任何角色 + verify(projectStaffRoleRepository, never()).save(any(ProjectStaffRole.class)); + } + + @Test + void assignStaffToProject_shouldUseDefaultStaffType_whenStaffTypeIsNull() { + // Given + UUID userId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + List roleIds = List.of(); + + User mockUser = new User(); + mockUser.setId(userId); + + ProjectStaff savedStaff = new ProjectStaff(); + savedStaff.setId(UUID.randomUUID()); + savedStaff.setUser(mockUser); + savedStaff.setProjectId(projectId); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(projectStaffRepository.findByUserIdAndProjectId(userId, projectId)).thenReturn(Optional.empty()); + when(projectStaffRepository.save(any(ProjectStaff.class))).thenAnswer(invocation -> { + ProjectStaff staff = invocation.getArgument(0); + staff.setId(UUID.randomUUID()); + return staff; + }); + + // When + ProjectStaff result = userManagementService.assignStaffToProject(userId, projectId, null, roleIds); + + // Then + assertNotNull(result); + assertEquals("GENERAL", result.getStaffType()); + } + + @Test + void assignStaffToProject_shouldReplaceRoles_whenCalledAgain() { + // Given: 用户已有一个角色 + UUID userId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + UUID oldRoleId = UUID.randomUUID(); + UUID newRoleId = UUID.randomUUID(); + String staffType = "GENERAL"; + List newRoleIds = List.of(newRoleId); + + User mockUser = new User(); + mockUser.setId(userId); + + Role oldRole = new Role(); + oldRole.setId(oldRoleId); + oldRole.setCode("OLD_ROLE"); + + Role newRole = new Role(); + newRole.setId(newRoleId); + newRole.setCode("NEW_ROLE"); + + ProjectStaffRole existingStaffRole = new ProjectStaffRole(); + existingStaffRole.setRole(oldRole); + + ProjectStaff existingStaff = new ProjectStaff(); + existingStaff.setId(UUID.randomUUID()); + existingStaff.setUser(mockUser); + existingStaff.setProjectId(projectId); + existingStaff.setStaffType("GENERAL"); // staffType相同,不会触发staff.save + existingStaff.setStaffRoles(new ArrayList<>(List.of(existingStaffRole))); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(projectStaffRepository.findByUserIdAndProjectId(userId, projectId)).thenReturn(Optional.of(existingStaff)); + when(roleService.findById(newRoleId)).thenReturn(newRole); + when(projectStaffRoleRepository.save(any(ProjectStaffRole.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // When: 重新传入新的角色列表 + ProjectStaff result = userManagementService.assignStaffToProject(userId, projectId, staffType, newRoleIds); + + // Then: 旧角色应该被删除,新角色应该被添加 + assertNotNull(result); + verify(projectStaffRoleRepository, times(1)).deleteAll(anyList()); + verify(projectStaffRoleRepository, times(1)).save(any(ProjectStaffRole.class)); + assertEquals(1, result.getStaffRoles().size()); + assertEquals(newRole, result.getStaffRoles().get(0).getRole()); + } + + @Test + void getProjectStaffsWithRoles_shouldReturnStaffList_withRoles() { + // Given + UUID projectId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + UUID roleId = UUID.randomUUID(); + + User mockUser = new User(); + mockUser.setId(userId); + mockUser.setUsername("testuser"); + mockUser.setRoles(new ArrayList<>()); + + Role mockRole = new Role(); + mockRole.setId(roleId); + mockRole.setCode("PROJECT_ADMIN"); + mockRole.setName("项目管理员"); + + ProjectStaffRole staffRole = new ProjectStaffRole(); + staffRole.setRole(mockRole); + + ProjectStaff staff = new ProjectStaff(); + staff.setId(UUID.randomUUID()); + staff.setUser(mockUser); + staff.setProjectId(projectId); + staff.setStaffRoles(new ArrayList<>(List.of(staffRole))); + + when(projectStaffRepository.findByProjectIdWithRoles(projectId)).thenReturn(List.of(staff)); + + // When + List result = userManagementService.getProjectStaffsWithRoles(projectId); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(mockUser, result.get(0).getUser()); + assertEquals(1, result.get(0).getStaffRoles().size()); + assertEquals(mockRole, result.get(0).getStaffRoles().get(0).getRole()); + } + + @Test + void getProjectStaffsWithRoles_shouldReturnEmptyList_whenNoStaffInProject() { + // Given + UUID projectId = UUID.randomUUID(); + + when(projectStaffRepository.findByProjectIdWithRoles(projectId)).thenReturn(List.of()); + + // When + List result = userManagementService.getProjectStaffsWithRoles(projectId); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void removeStaffFromProject_shouldDeleteStaff_whenExists() { + // Given + UUID userId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + + ProjectStaff existingStaff = new ProjectStaff(); + existingStaff.setId(UUID.randomUUID()); + existingStaff.setProjectId(projectId); + + when(projectStaffRepository.findByUserIdAndProjectId(userId, projectId)).thenReturn(Optional.of(existingStaff)); + doNothing().when(projectStaffRepository).delete(existingStaff); + + // When + userManagementService.removeStaffFromProject(userId, projectId); + + // Then + verify(projectStaffRepository).delete(existingStaff); + } + + @Test + void removeStaffFromProject_shouldDoNothing_whenStaffNotExists() { + // Given + UUID userId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + + when(projectStaffRepository.findByUserIdAndProjectId(userId, projectId)).thenReturn(Optional.empty()); + + // When + userManagementService.removeStaffFromProject(userId, projectId); + + // Then + verify(projectStaffRepository, never()).delete(any()); + } +} diff --git a/module-common/src/main/java/com/ether/pms/common/GlobalExceptionHandler.java b/module-common/src/main/java/com/ether/pms/common/GlobalExceptionHandler.java index f78f07d..1e3cdf0 100644 --- a/module-common/src/main/java/com/ether/pms/common/GlobalExceptionHandler.java +++ b/module-common/src/main/java/com/ether/pms/common/GlobalExceptionHandler.java @@ -1,11 +1,12 @@ package com.ether.pms.common; -import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import lombok.extern.slf4j.Slf4j; + @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @@ -13,24 +14,40 @@ public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity> handleBusinessException(BusinessException e) { log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage()); + HttpStatus status = mapErrorCodeToHttpStatus(e.getCode()); return ResponseEntity - .status(HttpStatus.OK) + .status(status) .body(ApiResponse.error(e.getCode(), e.getMessage())); } - + + private HttpStatus mapErrorCodeToHttpStatus(int errorCode) { + if (errorCode >= 400 && errorCode < 500) { + return HttpStatus.valueOf(errorCode); + } + return HttpStatus.OK; + } + @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { log.warn("参数异常: {}", e.getMessage()); return ResponseEntity - .status(HttpStatus.OK) + .status(HttpStatus.BAD_REQUEST) .body(ApiResponse.error(400, e.getMessage())); } - + + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity> handleIllegalStateException(IllegalStateException e) { + log.warn("状态异常: {}", e.getMessage()); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.error(400, e.getMessage())); + } + @ExceptionHandler(Exception.class) public ResponseEntity> handleException(Exception e) { log.error("系统异常", e); return ResponseEntity - .status(HttpStatus.OK) + .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error(ErrorCode.SYSTEM_ERROR.getCode(), "系统错误,请稍后重试")); } } diff --git a/module-mdm/pom.xml b/module-mdm/pom.xml index 37f889c..42a6c94 100644 --- a/module-mdm/pom.xml +++ b/module-mdm/pom.xml @@ -60,6 +60,12 @@ mapstruct + + org.apache.poi + poi-ooxml + 5.2.5 + + org.springframework.boot diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/controller/EnergyController.java b/module-mdm/src/main/java/com/ether/pms/mdm/controller/EnergyController.java index ab21701..0798153 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/controller/EnergyController.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/controller/EnergyController.java @@ -18,7 +18,7 @@ import java.util.Map; import java.util.UUID; @RestController -@RequestMapping("/api/v1/ops") +@RequestMapping("/api/ops/energy") @RequiredArgsConstructor public class EnergyController { @@ -27,12 +27,12 @@ public class EnergyController { // ==================== 计量点管理 ==================== - @PostMapping("/energy-meters") + @PostMapping("/meters") public ApiResponse createMeter(@Valid @RequestBody EnergyMeter meter) { return ApiResponse.success(energyMeterService.createMeter(meter)); } - @GetMapping("/energy-meters") + @GetMapping("/meters") public ApiResponse> getMeters( @RequestParam UUID projectId, @RequestParam(required = false) EnergyMeter.EnergyType energyType) { @@ -45,17 +45,17 @@ public class EnergyController { return ApiResponse.success(meters); } - @GetMapping("/energy-meters/{id}") + @GetMapping("/meters/{id}") public ApiResponse getMeter(@PathVariable UUID id) { return ApiResponse.success(energyMeterService.getMeterById(id)); } - @PutMapping("/energy-meters/{id}") + @PutMapping("/meters/{id}") public ApiResponse updateMeter(@PathVariable UUID id, @Valid @RequestBody EnergyMeter meter) { return ApiResponse.success(energyMeterService.updateMeter(id, meter)); } - @DeleteMapping("/energy-meters/{id}") + @DeleteMapping("/meters/{id}") public ApiResponse deleteMeter(@PathVariable UUID id) { energyMeterService.deleteMeter(id); return ApiResponse.success(null); @@ -63,14 +63,14 @@ public class EnergyController { // ==================== 能耗记录 ==================== - @PostMapping("/energy-consumption") + @PostMapping("/consumption") public ApiResponse recordConsumption(@RequestBody RecordConsumptionRequest request) { EnergyConsumption consumption = energyConsumptionService.recordConsumption( request.getMeterId(), request.getCurrentReading(), request.getRecordedBy()); return ApiResponse.success(consumption); } - @GetMapping("/energy-consumption/{meterId}") + @GetMapping("/consumption/{meterId}") public ApiResponse> getConsumption( @PathVariable UUID meterId, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, @@ -86,14 +86,14 @@ public class EnergyController { // ==================== 能耗统计 ==================== - @GetMapping("/energy-statistics/by-type") + @GetMapping("/statistics/by-type") public ApiResponse> getConsumptionByType( @RequestParam UUID projectId, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate month) { return ApiResponse.success(energyConsumptionService.getConsumptionByType(projectId, month)); } - @GetMapping("/energy-statistics/unit-consumption") + @GetMapping("/statistics/unit-consumption") public ApiResponse getUnitConsumption( @RequestParam UUID projectId, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate month) { diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionItemController.java b/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionItemController.java new file mode 100644 index 0000000..cc84b50 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionItemController.java @@ -0,0 +1,62 @@ +package com.ether.pms.mdm.controller; + +import com.ether.pms.common.ApiResponse; +import com.ether.pms.mdm.entity.InspectionItem; +import com.ether.pms.mdm.service.InspectionItemService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +/** + * 巡检标准项控制器 + */ +@RestController +@RequestMapping("/api/mdm/inspection-items") +@RequiredArgsConstructor +public class InspectionItemController { + + private final InspectionItemService inspectionItemService; + + @PostMapping + public ApiResponse createItem(@RequestBody InspectionItem item) { + return ApiResponse.success(inspectionItemService.createItem(item)); + } + + @GetMapping + public ApiResponse> getItems( + @RequestParam(required = false) String equipmentType, + @RequestParam(required = false) String systemType, + @RequestParam(required = false) Boolean activeOnly) { + List items; + if (activeOnly != null && activeOnly) { + items = inspectionItemService.getActiveItems(); + } else if (equipmentType != null && systemType != null) { + items = inspectionItemService.getItemsByEquipmentTypeAndSystemType(equipmentType, systemType); + } else if (equipmentType != null) { + items = inspectionItemService.getItemsByEquipmentType(equipmentType); + } else if (systemType != null) { + items = inspectionItemService.getItemsBySystemType(systemType); + } else { + items = inspectionItemService.getAllItems(); + } + return ApiResponse.success(items); + } + + @GetMapping("/{id}") + public ApiResponse getItem(@PathVariable UUID id) { + return ApiResponse.success(inspectionItemService.getItemById(id)); + } + + @PutMapping("/{id}") + public ApiResponse updateItem(@PathVariable UUID id, @RequestBody InspectionItem item) { + return ApiResponse.success(inspectionItemService.updateItem(id, item)); + } + + @DeleteMapping("/{id}") + public ApiResponse deleteItem(@PathVariable UUID id) { + inspectionItemService.deleteItem(id); + return ApiResponse.success(null); + } +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionRecordController.java b/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionRecordController.java new file mode 100644 index 0000000..8825dd6 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionRecordController.java @@ -0,0 +1,75 @@ +package com.ether.pms.mdm.controller; + +import com.ether.pms.common.ApiResponse; +import com.ether.pms.mdm.entity.InspectionRecord; +import com.ether.pms.mdm.service.InspectionRecordService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * 巡检记录控制器 + */ +@RestController +@RequestMapping("/api/mdm/inspection-records") +@RequiredArgsConstructor +public class InspectionRecordController { + + private final InspectionRecordService inspectionRecordService; + + @PostMapping + public ApiResponse createRecord(@RequestBody InspectionRecord record) { + return ApiResponse.success(inspectionRecordService.createRecord(record)); + } + + @GetMapping + public ApiResponse> getRecords( + @RequestParam(required = false) UUID equipmentId, + @RequestParam(required = false) UUID planId, + @RequestParam(required = false) String inspector, + @RequestParam(required = false) InspectionRecord.CheckStatus status, + @RequestParam(required = false) LocalDate startDate, + @RequestParam(required = false) LocalDate endDate) { + List records; + if (equipmentId != null && startDate != null && endDate != null) { + records = inspectionRecordService.getRecordsByEquipmentAndDateRange(equipmentId, startDate, endDate); + } else if (equipmentId != null) { + records = inspectionRecordService.getRecordsByEquipment(equipmentId); + } else if (planId != null) { + records = inspectionRecordService.getRecordsByPlan(planId); + } else if (inspector != null) { + records = inspectionRecordService.getRecordsByInspector(inspector); + } else if (status != null) { + records = inspectionRecordService.getRecordsByStatus(status); + } else if (startDate != null && endDate != null) { + records = inspectionRecordService.getRecordsByDateRange(startDate, endDate); + } else { + records = inspectionRecordService.getAllRecords(); + } + return ApiResponse.success(records); + } + + @GetMapping("/{id}") + public ApiResponse getRecord(@PathVariable UUID id) { + return ApiResponse.success(inspectionRecordService.getRecordById(id)); + } + + @PutMapping("/{id}") + public ApiResponse updateRecord(@PathVariable UUID id, @RequestBody InspectionRecord record) { + return ApiResponse.success(inspectionRecordService.updateRecord(id, record)); + } + + @DeleteMapping("/{id}") + public ApiResponse deleteRecord(@PathVariable UUID id) { + inspectionRecordService.deleteRecord(id); + return ApiResponse.success(null); + } + + @PostMapping("/{id}/complete") + public ApiResponse completeRecord(@PathVariable UUID id) { + return ApiResponse.success(inspectionRecordService.completeRecord(id)); + } +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionTemplateController.java b/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionTemplateController.java index a8f34cf..4ce51c9 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionTemplateController.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/controller/InspectionTemplateController.java @@ -11,7 +11,7 @@ import java.util.List; import java.util.UUID; @RestController -@RequestMapping("/api/v1/ops/inspection-templates") +@RequestMapping("/api/ops/inspection-templates") @RequiredArgsConstructor public class InspectionTemplateController { diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/controller/MaintenancePlanController.java b/module-mdm/src/main/java/com/ether/pms/mdm/controller/MaintenancePlanController.java deleted file mode 100644 index 80f5922..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/controller/MaintenancePlanController.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.ether.pms.mdm.controller; - -import com.ether.pms.common.ApiResponse; -import com.ether.pms.mdm.entity.MaintenancePlan; -import com.ether.pms.mdm.service.MaintenancePlanService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.UUID; - -@RestController -@RequestMapping("/api/v1/ops/maintenance-plans") -@RequiredArgsConstructor -public class MaintenancePlanController { - - private final MaintenancePlanService maintenancePlanService; - - @PostMapping - public ApiResponse createPlan(@Valid @RequestBody MaintenancePlan plan) { - MaintenancePlan created = maintenancePlanService.createPlan(plan); - return ApiResponse.success(created); - } - - @GetMapping - public ApiResponse> getPlans( - @RequestParam UUID projectId, - @RequestParam(required = false) MaintenancePlan.TriggerType triggerType) { - List plans; - if (triggerType != null) { - plans = maintenancePlanService.getPlansByTriggerType(triggerType); - } else { - plans = maintenancePlanService.getActivePlansByProject(projectId); - } - return ApiResponse.success(plans); - } - - @GetMapping("/{id}") - public ApiResponse getPlan(@PathVariable UUID id) { - return ApiResponse.success(maintenancePlanService.getPlanById(id)); - } - - @PutMapping("/{id}") - public ApiResponse updatePlan(@PathVariable UUID id, @Valid @RequestBody MaintenancePlan plan) { - return ApiResponse.success(maintenancePlanService.updatePlan(id, plan)); - } - - @DeleteMapping("/{id}") - public ApiResponse deactivatePlan(@PathVariable UUID id) { - maintenancePlanService.deactivatePlan(id); - return ApiResponse.success(null); - } -} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/controller/MaintenanceTaskController.java b/module-mdm/src/main/java/com/ether/pms/mdm/controller/MaintenanceTaskController.java deleted file mode 100644 index ac073ac..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/controller/MaintenanceTaskController.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.ether.pms.mdm.controller; - -import com.ether.pms.common.ApiResponse; -import com.ether.pms.mdm.entity.MaintenanceTask; -import com.ether.pms.mdm.service.MaintenanceTaskService; -import jakarta.validation.Valid; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -import java.math.BigDecimal; -import java.util.List; -import java.util.UUID; - -@RestController -@RequestMapping("/api/v1/ops/maintenance-tasks") -@RequiredArgsConstructor -public class MaintenanceTaskController { - - private final MaintenanceTaskService maintenanceTaskService; - - @GetMapping - public ApiResponse> getTasks( - @RequestParam(required = false) UUID projectId, - @RequestParam(required = false) UUID assignee, - @RequestParam(required = false) MaintenanceTask.Status status, - @RequestParam(required = false) UUID equipmentId) { - List tasks; - if (equipmentId != null) { - tasks = maintenanceTaskService.getTasksByEquipment(equipmentId); - } else if (assignee != null) { - tasks = maintenanceTaskService.getTasksByAssignee(assignee); - } else if (status != null && projectId != null) { - tasks = maintenanceTaskService.getTasksByStatus(projectId, status); - } else { - tasks = maintenanceTaskService.getOverdueTasks(); - } - return ApiResponse.success(tasks); - } - - @GetMapping("/{id}") - public ApiResponse getTask(@PathVariable UUID id) { - return ApiResponse.success(maintenanceTaskService.getTaskById(id)); - } - - @PostMapping - public ApiResponse createTask(@Valid @RequestBody MaintenanceTask task) { - return ApiResponse.success(maintenanceTaskService.createTask(task)); - } - - @PostMapping("/{id}/accept") - public ApiResponse acceptTask(@PathVariable UUID id, @RequestParam UUID userId) { - return ApiResponse.success(maintenanceTaskService.acceptTask(id, userId)); - } - - @PostMapping("/{id}/start") - public ApiResponse startTask(@PathVariable UUID id) { - return ApiResponse.success(maintenanceTaskService.startTask(id)); - } - - @PostMapping("/{id}/complete") - public ApiResponse completeTask( - @PathVariable UUID id, - @RequestBody CompleteTaskRequest request) { - return ApiResponse.success(maintenanceTaskService.completeTask( - id, request.getLaborHours(), request.getMaterialsCost(), request.getRemarks())); - } - - @PostMapping("/{id}/cancel") - public ApiResponse cancelTask(@PathVariable UUID id) { - maintenanceTaskService.cancelTask(id); - return ApiResponse.success(null); - } - - @Data - public static class CompleteTaskRequest { - private BigDecimal laborHours; - private BigDecimal materialsCost; - private String remarks; - } -} \ No newline at end of file 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 35ae085..364bc5f 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 @@ -4,6 +4,7 @@ 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.ProjectDeleteCheckVO; import com.ether.pms.mdm.dto.ProjectMemberDTO; import com.ether.pms.mdm.dto.ProjectQueryRequest; import com.ether.pms.mdm.dto.ProjectSelectorItem; @@ -170,4 +171,18 @@ public class ProjectController { @RequestBody ProjectConfigDTO dto) { return ResponseEntity.ok(ApiResponse.success(projectConfigService.updateConfig(projectId, dto))); } + + // ======================================== + // PM-009: 删除前检查 + // ======================================== + + /** + * PM-009: 项目删除前检查 + * 检查项目的关联数据情况,如果存在应收未收费用则无法删除 + */ + @GetMapping("/{projectId}/delete-check") + public ResponseEntity> checkProjectDelete( + @PathVariable UUID projectId) { + return ResponseEntity.ok(ApiResponse.success(projectService.checkProjectDelete(projectId))); + } } 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 24c66be..15940f4 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 @@ -2,21 +2,28 @@ 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.SpaceNodeDeleteCheckDTO; import com.ether.pms.mdm.dto.SpaceNodeEquipmentDTO; import com.ether.pms.mdm.dto.SpaceNodeTreeDTO; import com.ether.pms.mdm.dto.SpaceNodeUpdateDTO; +import com.ether.pms.mdm.dto.EquipmentCreateDTO; +import com.ether.pms.mdm.dto.FloorInfoVO; import com.ether.pms.mdm.entity.SpaceNode; import com.ether.pms.mdm.service.SpaceNodeService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; @RestController -@RequestMapping("/api/v1/mdm/space-nodes") +@RequestMapping("/api/mdm/space-nodes") @RequiredArgsConstructor public class SpaceNodeController { @@ -59,16 +66,16 @@ public class SpaceNodeController { return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findChildren(parentId))); } - @GetMapping("/parent/{parentCode}") - public ResponseEntity>> findByParent(@PathVariable String parentCode) { - return ResponseEntity.ok(ApiResponse.success(spaceNodeService.findByParentCode(parentCode))); - } - @PostMapping public ResponseEntity> create(@Valid @RequestBody SpaceNodeCreateDTO dto) { return ResponseEntity.ok(ApiResponse.success(spaceNodeService.create(dto))); } + @PostMapping("/batch") + public ResponseEntity>> batchCreate(@Valid @RequestBody List dtoList) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.batchCreate(dtoList))); + } + @PutMapping("/{id}") public ResponseEntity> update(@PathVariable UUID id, @RequestBody SpaceNodeUpdateDTO dto) { return ResponseEntity.ok(ApiResponse.success(spaceNodeService.update(id, dto))); @@ -80,6 +87,17 @@ public class SpaceNodeController { return ResponseEntity.ok(ApiResponse.success()); } + @GetMapping("/{id}/delete-check") + public ResponseEntity> checkDeleteInfo(@PathVariable UUID id) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.checkDeleteInfo(id))); + } + + @DeleteMapping("/{id}/cascade") + public ResponseEntity> deleteWithChildren(@PathVariable UUID id) { + spaceNodeService.deleteWithChildren(id); + return ResponseEntity.ok(ApiResponse.success()); + } + /** * 获取设备详情 */ @@ -115,4 +133,76 @@ public class SpaceNodeController { @RequestParam(defaultValue = "90") Integer daysAhead) { return ResponseEntity.ok(ApiResponse.success(spaceNodeService.getExpiringInspectionEquipment(projectId, daysAhead))); } + + /** + * 创建设备 + */ + @PostMapping("/equipment") + public ResponseEntity> createEquipment(@Valid @RequestBody EquipmentCreateDTO dto) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.createEquipment(dto))); + } + + /** + * 批量创建设备 + */ + @PostMapping("/equipment/batch") + public ResponseEntity>> batchCreateEquipment(@Valid @RequestBody List dtoList) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.batchCreateEquipment(dtoList))); + } + + /** + * Excel导入设备 + */ + @PostMapping("/equipment/import") + public ResponseEntity> importEquipment( + @RequestParam("file") MultipartFile file, + @RequestParam UUID projectId) { + return ResponseEntity.ok(spaceNodeService.importEquipmentFromExcel(file, projectId)); + } + + /** + * 获取楼栋楼层信息 + */ + @GetMapping("/{buildingId}/floor-info") + public ResponseEntity> getBuildingFloorInfo(@PathVariable String buildingId) { + return ResponseEntity.ok(ApiResponse.success(spaceNodeService.getBuildingFloorInfo(buildingId))); + } + + /** + * 调试端点:检查房间的楼层号数据 + */ + @GetMapping("/debug/floor-numbers") + public ResponseEntity>> debugFloorNumbers(@RequestParam UUID projectId) { + Map result = new HashMap<>(); + List allNodes = spaceNodeService.findByProjectId(projectId); + + // 统计房间楼层信息 + List> roomInfos = new ArrayList<>(); + long withFloor = 0; + long withoutFloor = 0; + + for (SpaceNode node : allNodes) { + if (node.getNodeType() == SpaceNode.NodeType.ROOM) { + Map info = new HashMap<>(); + info.put("id", node.getId()); + info.put("name", node.getName()); + info.put("floorNumber", node.getFloorNumber()); + info.put("parentId", node.getParentId()); + roomInfos.add(info); + + if (node.getFloorNumber() != null) { + withFloor++; + } else { + withoutFloor++; + } + } + } + + result.put("totalRooms", roomInfos.size()); + result.put("withFloorNumber", withFloor); + result.put("withoutFloorNumber", withoutFloor); + result.put("rooms", roomInfos.subList(0, Math.min(20, roomInfos.size()))); + + return ResponseEntity.ok(ApiResponse.success(result)); + } } diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/controller/SparePartController.java b/module-mdm/src/main/java/com/ether/pms/mdm/controller/SparePartController.java index a7335b6..5e075b4 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/controller/SparePartController.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/controller/SparePartController.java @@ -14,7 +14,7 @@ import java.util.List; import java.util.UUID; @RestController -@RequestMapping("/api/v1/ops/spare-parts") +@RequestMapping("/api/ops/spare-parts") @RequiredArgsConstructor public class SparePartController { diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/EquipmentCreateDTO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/EquipmentCreateDTO.java new file mode 100644 index 0000000..994950b --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/EquipmentCreateDTO.java @@ -0,0 +1,60 @@ +package com.ether.pms.mdm.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.UUID; + +@Data +public class EquipmentCreateDTO { + + @NotBlank(message = "设备名称不能为空") + private String name; + + @NotNull(message = "项目ID不能为空") + private UUID projectId; + + private UUID spaceNodeId; + + private Boolean isEquipment = true; + + private Integer designLifeYears; + + private BigDecimal ratedPower; + + private BigDecimal ratedVoltage; + + private BigDecimal ratedCurrent; + + private String maintenanceVendor; + + private String maintenanceVendorContact; + + private String maintenanceVendorPhone; + + private String maintenanceContractNo; + + private LocalDate maintenanceContractStart; + + private LocalDate maintenanceContractEnd; + + private String specialEquipmentType; + + private String specialEquipmentCert; + + private Integer inspectionCycle; + + private LocalDate nextInspectionDate; + + private LocalDate lastInspectionDate; + + private String lastInspectionResult; + + private BigDecimal energyConsumptionStandard; + + private String installationEnvironment; + + private String protectionLevel; +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/FloorDetailVO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/FloorDetailVO.java new file mode 100644 index 0000000..9193bec --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/FloorDetailVO.java @@ -0,0 +1,34 @@ +package com.ether.pms.mdm.dto; + +import lombok.Data; + +/** + * 楼层详情VO + */ +@Data +public class FloorDetailVO { + /** + * 楼层号(1=1楼,-1=地下一层) + */ + private Integer floorNumber; + + /** + * 是否有房间 + */ + private Boolean hasRooms; + + /** + * 是否有商铺 + */ + private Boolean hasShop; + + /** + * 房间数量 + */ + private Integer roomCount; + + /** + * 备注(如"大堂") + */ + private String remark; +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/FloorInfoVO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/FloorInfoVO.java new file mode 100644 index 0000000..7fd05b2 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/FloorInfoVO.java @@ -0,0 +1,35 @@ +package com.ether.pms.mdm.dto; + +import lombok.Data; +import java.util.List; + +/** + * 楼栋楼层信息VO + */ +@Data +public class FloorInfoVO { + /** + * 楼栋ID + */ + private String buildingId; + + /** + * 楼栋名称 + */ + private String buildingName; + + /** + * 总楼层数(地上) + */ + private Integer totalFloors; + + /** + * 地下楼层数 + */ + private Integer undergroundFloors; + + /** + * 楼层详情列表 + */ + private List floors; +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectDeleteCheckVO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectDeleteCheckVO.java new file mode 100644 index 0000000..92146d6 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectDeleteCheckVO.java @@ -0,0 +1,26 @@ +package com.ether.pms.mdm.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 项目删除检查返回对象 + */ +@Data +public class ProjectDeleteCheckVO { + /** + * 是否可以删除 + */ + private Boolean canDelete; + + /** + * 无法删除的原因 + */ + private String reason; + + /** + * 项目关联数据统计 + */ + private ProjectDeleteStatistics statistics; +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectDeleteStatistics.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectDeleteStatistics.java new file mode 100644 index 0000000..92ccfeb --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/ProjectDeleteStatistics.java @@ -0,0 +1,41 @@ +package com.ether.pms.mdm.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 项目删除检查统计数据 + */ +@Data +public class ProjectDeleteStatistics { + /** + * 空间数量 + */ + private Integer spaceCount; + + /** + * 成员数量 + */ + private Integer memberCount; + + /** + * 设施设备数量 + */ + private Integer equipmentCount; + + /** + * 账单数量 + */ + private Integer billCount; + + /** + * 应收总额 + */ + private BigDecimal totalReceivable; + + /** + * 未收金额 + */ + private BigDecimal unpaidAmount; +} \ No newline at end of file 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 index c44f70c..5879d29 100644 --- 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 @@ -13,9 +13,6 @@ public class SpaceNodeCreateDTO { @NotNull(message = "项目ID不能为空") private UUID projectId; - @NotBlank(message = "空间节点代码不能为空") - private String code; - @NotBlank(message = "空间节点名称不能为空") private String name; @@ -23,12 +20,12 @@ public class SpaceNodeCreateDTO { private String shortName; - @NotNull(message = "节点大类不能为空") private SpaceNode.NodeCategory nodeCategory; - @NotNull(message = "空间节点类型不能为空") private SpaceNode.NodeType nodeType; + private String spaceType; + private String usageType; private UUID parentId; diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeDTO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeDTO.java index a20cd27..38557ce 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeDTO.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeDTO.java @@ -9,7 +9,6 @@ import java.util.UUID; public class SpaceNodeDTO { private UUID id; private UUID projectId; - private String code; private String name; private String fullName; private String shortName; @@ -17,7 +16,6 @@ public class SpaceNodeDTO { private SpaceNode.NodeType nodeType; private String nodeTypeName; private UUID parentId; - private String parentCode; private String treePath; private String treePathName; private Integer level; diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeDeleteCheckDTO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeDeleteCheckDTO.java new file mode 100644 index 0000000..ad7637f --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeDeleteCheckDTO.java @@ -0,0 +1,33 @@ +package com.ether.pms.mdm.dto; + +import java.util.Map; +import java.util.UUID; + +public class SpaceNodeDeleteCheckDTO { + private UUID nodeId; + private String nodeName; + private int childCount; + private Map childTypeCount; + private int totalDescendantCount; + + public SpaceNodeDeleteCheckDTO() {} + + public SpaceNodeDeleteCheckDTO(UUID nodeId, String nodeName, int childCount, Map childTypeCount, int totalDescendantCount) { + this.nodeId = nodeId; + this.nodeName = nodeName; + this.childCount = childCount; + this.childTypeCount = childTypeCount; + this.totalDescendantCount = totalDescendantCount; + } + + public UUID getNodeId() { return nodeId; } + public void setNodeId(UUID nodeId) { this.nodeId = nodeId; } + public String getNodeName() { return nodeName; } + public void setNodeName(String nodeName) { this.nodeName = nodeName; } + public int getChildCount() { return childCount; } + public void setChildCount(int childCount) { this.childCount = childCount; } + public Map getChildTypeCount() { return childTypeCount; } + public void setChildTypeCount(Map childTypeCount) { this.childTypeCount = childTypeCount; } + public int getTotalDescendantCount() { return totalDescendantCount; } + public void setTotalDescendantCount(int totalDescendantCount) { this.totalDescendantCount = totalDescendantCount; } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeEquipmentDTO.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeEquipmentDTO.java index 3305589..2f53abc 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeEquipmentDTO.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeEquipmentDTO.java @@ -13,7 +13,7 @@ public class SpaceNodeEquipmentDTO extends SpaceNodeDTO { private Boolean isEquipment; private Integer designLifeYears; private BigDecimal ratedPower; - private String ratedVoltage; + private BigDecimal ratedVoltage; private BigDecimal ratedCurrent; private String maintenanceVendor; private String maintenanceVendorContact; diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeProjection.java b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeProjection.java new file mode 100644 index 0000000..8606426 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/dto/SpaceNodeProjection.java @@ -0,0 +1,67 @@ +package com.ether.pms.mdm.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +public interface SpaceNodeProjection { + UUID getId(); + String getProjectId(); + String getName(); + String getFullName(); + String getShortName(); + String getNodeCategory(); + String getNodeType(); + String getUsageType(); + UUID getParentId(); + String getTreePath(); + String getTreePathName(); + Integer getLevel(); + Integer getSortOrder(); + String getStatus(); + String getDeliveryStatus(); + String getDecorationStatus(); + BigDecimal getBuildingArea(); + BigDecimal getUsableArea(); + BigDecimal getSharedArea(); + BigDecimal getLandArea(); + BigDecimal getLongitude(); + BigDecimal getLatitude(); + BigDecimal getAltitude(); + Integer getFloorNumber(); + String getProvince(); + String getCity(); + String getDistrict(); + String getStreet(); + String getAddress(); + Boolean getIsEquipment(); + String getSpecialEquipmentType(); + String getSpecialEquipmentCert(); + LocalDate getLastInspectionDate(); + String getLastInspectionResult(); + LocalDate getNextInspectionDate(); + Integer getInspectionCycle(); + String getProtectionLevel(); + String getMaintenanceVendor(); + String getMaintenanceVendorContact(); + String getMaintenanceVendorPhone(); + LocalDate getMaintenanceContractStart(); + LocalDate getMaintenanceContractEnd(); + String getMaintenanceContractNo(); + String getRatedVoltage(); + String getRatedCurrent(); + String getRatedPower(); + String getEnergyConsumptionStandard(); + String getInstallationEnvironment(); + String getDesignLifeYears(); + String getCommonSpareParts(); + String getAttributes(); + String getNodeCategoryName(); + String getNodeTypeName(); + LocalDateTime getCreatedAt(); + LocalDateTime getUpdatedAt(); + String getCreatedBy(); + String getUpdatedBy(); + Boolean getIsDeleted(); +} 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 index 1ac8a1d..3b690a7 100644 --- 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 @@ -10,7 +10,6 @@ import java.util.UUID; @Data public class SpaceNodeTreeDTO { private UUID id; - private String code; private String name; private String fullName; private SpaceNode.NodeCategory nodeCategory; @@ -29,7 +28,6 @@ public class SpaceNodeTreeDTO { 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()); diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/InspectionItem.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/InspectionItem.java new file mode 100644 index 0000000..f534e79 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/entity/InspectionItem.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_inspection_item") +@Data +public class InspectionItem { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "equipment_type", length = 50) + private String equipmentType; + + @Column(name = "system_type", length = 50) + private String systemType; + + @Column(name = "item_name", nullable = false, length = 200) + private String itemName; + + @Column(name = "check_method", length = 200) + private String checkMethod; + + @Column(name = "standard_value", length = 100) + private String standardValue; + + @Column(name = "is_required") + private Boolean isRequired = true; + + @Column(length = 500) + private String remark; + + @Column(name = "sort_order") + private Integer sortOrder; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Status status = Status.ACTIVE; + + public enum Status { + ACTIVE, INACTIVE + } + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + public void prePersist() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/InspectionRecord.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/InspectionRecord.java new file mode 100644 index 0000000..60acc3f --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/entity/InspectionRecord.java @@ -0,0 +1,82 @@ +package com.ether.pms.mdm.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * 巡检记录实体 + */ +@Entity +@Table(name = "mdm_inspection_record") +@Data +public class InspectionRecord { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "plan_id") + private UUID planId; + + @Column(name = "equipment_id", nullable = false) + private UUID equipmentId; + + @Column(name = "inspection_date", nullable = false) + private LocalDate inspectionDate; + + @Column(name = "inspector", nullable = false, length = 200) + private String inspector; + + @Column(length = 20) + @Enumerated(EnumType.STRING) + private CheckStatus status = CheckStatus.NORMAL; + + public enum CheckStatus { + NORMAL, WARNING, ABNORMAL + } + + @Column(name = "check_in_time") + private LocalDateTime checkInTime; + + @Column(name = "check_in_location", length = 100) + private String checkInLocation; + + @Column(name = "check_in_photo", length = 200) + private String checkInPhoto; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "items", columnDefinition = "jsonb") + private List> items; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "problems", columnDefinition = "jsonb") + private List> problems; + + @Column + private Boolean completed = false; + + @Column(name = "completed_time") + private LocalDateTime completedTime; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @PrePersist + public void prePersist() { + createdAt = LocalDateTime.now(); + if (status == null) { + status = CheckStatus.NORMAL; + } + if (completed == null) { + completed = false; + } + } +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/MaintenancePlan.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/MaintenancePlan.java deleted file mode 100644 index cb0a1d3..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/entity/MaintenancePlan.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.ether.pms.mdm.entity; - -import jakarta.persistence.*; -import lombok.Data; -import java.time.LocalDateTime; -import java.util.UUID; - -@Entity -@Table(name = "ops_maintenance_plan") -@Data -public class MaintenancePlan { - - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - - @Column(name = "project_id", nullable = false) - private UUID projectId; - - @Column(name = "plan_code", nullable = false, unique = true) - private String planCode; - - @Column(name = "plan_name", nullable = false) - private String planName; - - @Column(name = "equipment_type") - private String equipmentType; - - @Column(name = "trigger_type", nullable = false) - @Enumerated(EnumType.STRING) - private TriggerType triggerType; - - public enum TriggerType { - TIME_BASED, // 时间触发 - HOURS_BASED, // 运行小时触发 - CYCLES_BASED, // 次数触发 - CONDITION_BASED // 条件触发 - } - - @Column(name = "trigger_value") - private Integer triggerValue; - - @Column(name = "trigger_unit") - private String triggerUnit; - - @Column(name = "maintenance_items", columnDefinition = "TEXT") - private String maintenanceItems; - - @Column(name = "estimated_duration") - private Integer estimatedDuration; - - @Column(name = "assigned_to") - private UUID assignedTo; - - @Column(name = "sla_response_hours") - private Integer slaResponseHours; - - @Column(name = "sla_complete_hours") - private Integer slaCompleteHours; - - @Column(nullable = false) - @Enumerated(EnumType.STRING) - private Status status = Status.ACTIVE; - - public enum Status { - ACTIVE, INACTIVE - } - - @Column(name = "created_at") - private LocalDateTime createdAt; - - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - @PrePersist - public void prePersist() { - createdAt = LocalDateTime.now(); - updatedAt = LocalDateTime.now(); - } - - @PreUpdate - public void preUpdate() { - updatedAt = LocalDateTime.now(); - } -} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/MaintenanceTask.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/MaintenanceTask.java deleted file mode 100644 index 737933b..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/entity/MaintenanceTask.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.ether.pms.mdm.entity; - -import jakarta.persistence.*; -import lombok.Data; -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.UUID; - -@Entity -@Table(name = "ops_maintenance_task") -@Data -public class MaintenanceTask { - - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - - @Column(name = "project_id", nullable = false) - private UUID projectId; - - @Column(name = "task_code", nullable = false, unique = true) - private String taskCode; - - @Column(name = "plan_id") - private UUID planId; - - @Column(name = "equipment_id") - private UUID equipmentId; - - @Column(name = "task_type") - @Enumerated(EnumType.STRING) - private TaskType taskType = TaskType.PREVENTIVE; - - public enum TaskType { - PREVENTIVE, // 预防性维护 - CORRECTIVE // 纠正性维护 - } - - @Column(name = "trigger_type") - @Enumerated(EnumType.STRING) - private MaintenancePlan.TriggerType triggerType; - - @Column(name = "maintenance_items", columnDefinition = "TEXT") - private String maintenanceItems; - - @Column(nullable = false) - @Enumerated(EnumType.STRING) - private Status status = Status.PENDING; - - public enum Status { - PENDING, // 待接受 - ACCEPTED, // 已接受 - IN_PROGRESS, // 执行中 - COMPLETED, // 已完成 - CANCELLED // 已取消 - } - - @Column(name = "assigned_to") - private UUID assignedTo; - - @Column(name = "scheduled_date") - private LocalDateTime scheduledDate; - - @Column(name = "actual_start_date") - private LocalDateTime actualStartDate; - - @Column(name = "actual_end_date") - private LocalDateTime actualEndDate; - - @Column(name = "labor_hours", precision = 10, scale = 2) - private BigDecimal laborHours; - - @Column(name = "materials_cost", precision = 12, scale = 2) - private BigDecimal materialsCost; - - @Column(columnDefinition = "TEXT") - private String remarks; - - @Column(name = "created_at") - private LocalDateTime createdAt; - - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - @PrePersist - public void prePersist() { - createdAt = LocalDateTime.now(); - updatedAt = LocalDateTime.now(); - } - - @PreUpdate - public void preUpdate() { - updatedAt = LocalDateTime.now(); - } -} \ No newline at end of file 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 2d7529d..1b8cc12 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 @@ -25,17 +25,11 @@ public class SpaceNode { private UUID id; @NotNull(message = "项目ID不能为空") - @Column(name = "project_id", nullable = false) + @Column(name = "project_code", nullable = false, length = 50) 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位之间") + @Size(min = 1, max = 100, message = "空间节点名称长度必须在1-100位之间") @Column(nullable = false, length = 100) private String name; @@ -61,9 +55,6 @@ public class SpaceNode { @Column(name = "parent_id") private UUID parentId; - @Column(name = "parent_code", length = 50) - private String parentCode; - @Column(name = "tree_path", length = 1000) private String treePath; @@ -97,10 +88,10 @@ public class SpaceNode { @Column(name = "land_area", precision = 10, scale = 2) private BigDecimal landArea; - @Column(precision = 10, scale = 6) + @Column(precision = 10) private BigDecimal longitude; - @Column(precision = 10, scale = 6) + @Column(precision = 10) private BigDecimal latitude; @Column(precision = 8, scale = 2) @@ -152,8 +143,8 @@ public class SpaceNode { @Column(name = "rated_power", precision = 10, scale = 2) private BigDecimal ratedPower; - @Column(name = "rated_voltage", length = 20) - private String ratedVoltage; + @Column(name = "rated_voltage", precision = 10, scale = 2) + private BigDecimal ratedVoltage; @Column(name = "rated_current", precision = 10, scale = 2) private BigDecimal ratedCurrent; @@ -259,6 +250,7 @@ public class SpaceNode { EQUIPMENT_ROOM("设备房", NodeCategory.FACILITY, 1), PROPERTY_OFFICE("物业用房", NodeCategory.FACILITY, 1), SECURITY_ROOM("门岗", NodeCategory.FACILITY, 1), + PUBLIC_ROOM("公共用房", NodeCategory.FACILITY, 1), PUBLIC_AREA("公共区域", NodeCategory.AREA, 1), GREEN_AREA("绿化区域", NodeCategory.AREA, 1), ROAD("道路", NodeCategory.AREA, 1); diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/enums/EquipmentType.java b/module-mdm/src/main/java/com/ether/pms/mdm/enums/EquipmentType.java index e002bf7..0b06c93 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/enums/EquipmentType.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/enums/EquipmentType.java @@ -1,31 +1,24 @@ package com.ether.pms.mdm.enums; public enum EquipmentType { - CENTRAL_AC("中央空调", EquipmentCategory.HVAC), - AIR_CONDITIONER("分体空调", EquipmentCategory.HVAC), - AIR_HANDLING_UNIT("空气处理机组", EquipmentCategory.HVAC), - FAN_COIL("风机盘管", EquipmentCategory.HVAC), - LOW_VOLTAGE_CABINET("低压配电柜", EquipmentCategory.ELECTRICAL), - TRANSFORMER("变压器", EquipmentCategory.ELECTRICAL), - GENERATOR("发电机", EquipmentCategory.ELECTRICAL), - UPS("不间断电源", EquipmentCategory.ELECTRICAL), - FIRE_PUMP("消防泵", EquipmentCategory.FIRE), - SPRINKLER("喷淋系统", EquipmentCategory.FIRE), - FIRE_ALARM("火灾报警系统", EquipmentCategory.FIRE), - ELEVATOR("电梯", EquipmentCategory.ELEVATOR), - CCTV("监控系统", EquipmentCategory.SECURITY), - ACCESS_CONTROL("门禁系统", EquipmentCategory.SECURITY), - WATER_PUMP("给水泵", EquipmentCategory.WATER_DRAINAGE), - DRAINAGE_PUMP("排水泵", EquipmentCategory.WATER_DRAINAGE), - LED_LIGHT("LED灯具", EquipmentCategory.LIGHTING), - HIGH_BAY_LIGHT("工矿灯", EquipmentCategory.LIGHTING); + ELEVATOR("电梯系统"), + HVAC("暖通空调"), + FIRE_PROTECTION("消防系统"), + PLUMBING("给排水系统"), + ELECTRICAL("电气系统"), + ENERGY_METER("能源计量"), + SECURITY("弱电系统"), + LANDSCAPE("景观绿化"), + KITCHEN("厨余设备"), + OTHER("其他设备"); - private final String desc; - private final EquipmentCategory category; - EquipmentType(String desc, EquipmentCategory category) { - this.desc = desc; - this.category = category; + private final String description; + + EquipmentType(String description) { + this.description = description; } - public String getDesc() { return desc; } - public EquipmentCategory getCategory() { return category; } -} \ No newline at end of file + + public String getDescription() { + return description; + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/enums/OwnershipType.java b/module-mdm/src/main/java/com/ether/pms/mdm/enums/OwnershipType.java new file mode 100644 index 0000000..a27a015 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/enums/OwnershipType.java @@ -0,0 +1,18 @@ +package com.ether.pms.mdm.enums; + +public enum OwnershipType { + PROJECT("项目自有"), + COMPANY("公司统筹"), + OWNER("业主自置"), + RENTAL("租赁设备"); + + private final String description; + + OwnershipType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/enums/SystemType.java b/module-mdm/src/main/java/com/ether/pms/mdm/enums/SystemType.java new file mode 100644 index 0000000..fa51d91 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/enums/SystemType.java @@ -0,0 +1,26 @@ +package com.ether.pms.mdm.enums; + +/** + * 商业地产8大系统分类枚举 + */ +public enum SystemType { + HVAC("暖通空调"), + FIRE("消防系统"), + ELEVATOR("电梯系统"), + ELECTRICAL("电气系统"), + PLUMBING("给排水"), + BAS("弱电智能化"), + KITCHEN("餐饮厨房"), + LANDSCAPE("景观"), + OTHER("其他"); + + private final String description; + + SystemType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/EnergyConsumptionRepository.java b/module-mdm/src/main/java/com/ether/pms/mdm/repository/EnergyConsumptionRepository.java index c1c45f6..208fc68 100644 --- a/module-mdm/src/main/java/com/ether/pms/mdm/repository/EnergyConsumptionRepository.java +++ b/module-mdm/src/main/java/com/ether/pms/mdm/repository/EnergyConsumptionRepository.java @@ -21,8 +21,8 @@ public interface EnergyConsumptionRepository extends JpaRepository findTopByMeterIdOrderByConsumptionDateDesc(UUID meterId); } \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/InspectionItemRepository.java b/module-mdm/src/main/java/com/ether/pms/mdm/repository/InspectionItemRepository.java new file mode 100644 index 0000000..cbee07e --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/repository/InspectionItemRepository.java @@ -0,0 +1,33 @@ +package com.ether.pms.mdm.repository; + +import com.ether.pms.mdm.entity.InspectionItem; +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.UUID; + +/** + * 巡检标准项Repository + */ +@Repository +public interface InspectionItemRepository extends JpaRepository { + + List findByEquipmentType(String equipmentType); + + List findBySystemType(String systemType); + + List findByStatus(InspectionItem.Status status); + + List findByEquipmentTypeAndStatus(String equipmentType, InspectionItem.Status status); + + List findBySystemTypeAndStatus(String systemType, InspectionItem.Status status); + + @Query("SELECT i FROM InspectionItem i WHERE i.equipmentType = :equipmentType AND i.systemType = :systemType AND i.status = 'ACTIVE' ORDER BY i.sortOrder") + List findByEquipmentTypeAndSystemType(@Param("equipmentType") String equipmentType, @Param("systemType") String systemType); + + @Query("SELECT i FROM InspectionItem i WHERE i.status = 'ACTIVE' ORDER BY i.sortOrder") + List findAllActiveOrderBySortOrder(); +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/InspectionRecordRepository.java b/module-mdm/src/main/java/com/ether/pms/mdm/repository/InspectionRecordRepository.java new file mode 100644 index 0000000..7aa3450 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/repository/InspectionRecordRepository.java @@ -0,0 +1,36 @@ +package com.ether.pms.mdm.repository; + +import com.ether.pms.mdm.entity.InspectionRecord; +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.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * 巡检记录Repository + */ +@Repository +public interface InspectionRecordRepository extends JpaRepository { + + List findByEquipmentId(UUID equipmentId); + + List findByPlanId(UUID planId); + + List findByInspector(String inspector); + + List findByStatus(InspectionRecord.CheckStatus status); + + List findByInspectionDate(LocalDate inspectionDate); + + List findByEquipmentIdAndInspectionDateBetween(UUID equipmentId, LocalDate startDate, LocalDate endDate); + + @Query("SELECT r FROM InspectionRecord r WHERE r.equipmentId = :equipmentId ORDER BY r.inspectionDate DESC") + List findByEquipmentIdOrderByInspectionDateDesc(@Param("equipmentId") UUID equipmentId); + + @Query("SELECT r FROM InspectionRecord r WHERE r.inspectionDate BETWEEN :startDate AND :endDate ORDER BY r.inspectionDate") + List findByDateRange(@Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/MaintenancePlanRepository.java b/module-mdm/src/main/java/com/ether/pms/mdm/repository/MaintenancePlanRepository.java deleted file mode 100644 index 8195083..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/repository/MaintenancePlanRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.ether.pms.mdm.repository; - -import com.ether.pms.mdm.entity.MaintenancePlan; -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 MaintenancePlanRepository extends JpaRepository { - List findByProjectIdAndStatus(UUID projectId, MaintenancePlan.Status status); - Optional findByPlanCode(String planCode); - List findByTriggerType(MaintenancePlan.TriggerType triggerType); - List findByStatusAndTriggerType(MaintenancePlan.Status status, MaintenancePlan.TriggerType triggerType); - boolean existsByPlanCode(String planCode); -} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/repository/MaintenanceTaskRepository.java b/module-mdm/src/main/java/com/ether/pms/mdm/repository/MaintenanceTaskRepository.java deleted file mode 100644 index cefb70b..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/repository/MaintenanceTaskRepository.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.ether.pms.mdm.repository; - -import com.ether.pms.mdm.entity.MaintenanceTask; -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.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Repository -public interface MaintenanceTaskRepository extends JpaRepository { - List findByProjectIdAndStatus(UUID projectId, MaintenanceTask.Status status); - List findByAssignedToAndStatus(UUID assignedTo, MaintenanceTask.Status status); - List findByEquipmentIdAndStatusNot(UUID equipmentId, MaintenanceTask.Status status); - Optional findByTaskCode(String taskCode); - boolean existsByTaskCode(String taskCode); - - @Query("SELECT t FROM MaintenanceTask t WHERE t.projectId = :projectId AND t.status IN :statuses") - List findByProjectIdAndStatusIn(@Param("projectId") UUID projectId, @Param("statuses") List statuses); - - @Query("SELECT t FROM MaintenanceTask t WHERE t.scheduledDate < :date AND t.status = 'PENDING'") - List findOverdueTasks(@Param("date") LocalDateTime date); - - List findByEquipmentIdAndStatus(UUID equipmentId, MaintenanceTask.Status status); -} \ No newline at end of file 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 5c53c41..fd387a8 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 @@ -37,4 +37,7 @@ public interface ProjectRepository extends JpaRepository, JpaSpec long countByStatus(@Param("status") String status); boolean existsByCodeAndIdNot(String code, UUID id); + + @Query(value = "SELECT COUNT(*) FROM equipment WHERE project_id = :projectId AND is_deleted = false", nativeQuery = true) + long countEquipmentsByProjectId(@Param("projectId") 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 b2a20b4..3269abb 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 @@ -14,8 +14,6 @@ import java.util.UUID; @Repository public interface SpaceNodeRepository extends JpaRepository { - boolean existsByProjectIdAndCode(UUID projectId, String code); - List findByIsDeletedFalse(); Optional findByIdAndIsDeletedFalse(UUID id); @@ -28,16 +26,24 @@ public interface SpaceNodeRepository extends JpaRepository { List findByParentIdAndIsDeletedFalseOrderBySortOrderAsc(UUID parentId); - List findByParentCodeAndIsDeletedFalse(String parentCode); - - // ========== 设备相关查询 ========== List findByProjectIdAndIsEquipment(UUID projectId, Boolean isEquipment); List findByProjectIdAndIsEquipmentAndSpecialEquipmentTypeIsNotNull(UUID projectId, Boolean isEquipment); - @Query("SELECT s FROM SpaceNode s WHERE s.projectId = :projectId AND s.isEquipment = :isEquipment AND s.nextInspectionDate <= :date") - List findByProjectIdAndIsEquipmentAndNextInspectionDateBefore( - @Param("projectId") UUID projectId, - @Param("isEquipment") Boolean isEquipment, - @Param("date") LocalDate date); + List findByProjectIdAndIsEquipmentAndNextInspectionDateBefore(UUID projectId, Boolean isEquipment, LocalDate date); + + /** + * 查询楼栋下所有房间和商铺(按楼层号分组) + */ + List findByParentIdAndNodeTypeInAndIsDeletedFalse(UUID parentId, List nodeTypes); + + /** + * 统计项目下的空间数量 + */ + long countByProjectIdAndIsDeletedFalse(UUID projectId); + + /** + * 统计项目下指定类型空间数量 + */ + long countByProjectIdAndNodeTypeAndIsDeletedFalse(UUID projectId, SpaceNode.NodeType nodeType); } diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/scheduler/MaintenanceScheduler.java b/module-mdm/src/main/java/com/ether/pms/mdm/scheduler/MaintenanceScheduler.java deleted file mode 100644 index 6c7f212..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/scheduler/MaintenanceScheduler.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.ether.pms.mdm.scheduler; - -import com.ether.pms.mdm.entity.MaintenancePlan; -import com.ether.pms.mdm.service.MaintenancePlanService; -import com.ether.pms.mdm.service.MaintenanceTaskService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Slf4j -@Component -@RequiredArgsConstructor -public class MaintenanceScheduler { - - private final MaintenancePlanService maintenancePlanService; - private final MaintenanceTaskService maintenanceTaskService; - - /** - * 检查时间触发的维保计划 - 每天凌晨1点执行 - */ - @Scheduled(cron = "0 0 1 * * ?") - public void checkTimeBasedMaintenance() { - log.info("开始检查时间触发的维保计划..."); - try { - List plans = maintenancePlanService.getPlansByTriggerType(MaintenancePlan.TriggerType.TIME_BASED); - for (MaintenancePlan plan : plans) { - try { - maintenanceTaskService.generateTasksFromPlan(plan); - log.info("为计划[{}]生成维保任务完成", plan.getPlanCode()); - } catch (Exception e) { - log.error("为计划[{}]生成维保任务失败: {}", plan.getPlanCode(), e.getMessage()); - } - } - log.info("时间触发维保计划检查完成,共处理 {} 个计划", plans.size()); - } catch (Exception e) { - log.error("检查时间触发维保计划失败: {}", e.getMessage(), e); - } - } - - /** - * 检查逾期任务 - 每小时执行 - */ - @Scheduled(cron = "0 0 * * * ?") - public void checkOverdueTasks() { - log.info("开始检查逾期维保任务..."); - try { - List overdueTasks = maintenanceTaskService.getOverdueTasks(); - if (!overdueTasks.isEmpty()) { - log.warn("发现 {} 个逾期维保任务", overdueTasks.size()); - // 可以发送通知等后续处理 - } - } catch (Exception e) { - log.error("检查逾期维保任务失败: {}", e.getMessage(), e); - } - } -} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/InspectionItemService.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/InspectionItemService.java new file mode 100644 index 0000000..7f155fa --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/service/InspectionItemService.java @@ -0,0 +1,30 @@ +package com.ether.pms.mdm.service; + +import com.ether.pms.mdm.entity.InspectionItem; + +import java.util.List; +import java.util.UUID; + +/** + * 巡检标准项服务接口 + */ +public interface InspectionItemService { + + InspectionItem createItem(InspectionItem item); + + InspectionItem updateItem(UUID id, InspectionItem item); + + void deleteItem(UUID id); + + InspectionItem getItemById(UUID id); + + List getAllItems(); + + List getItemsByEquipmentType(String equipmentType); + + List getItemsBySystemType(String systemType); + + List getActiveItems(); + + List getItemsByEquipmentTypeAndSystemType(String equipmentType, String systemType); +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/InspectionRecordService.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/InspectionRecordService.java new file mode 100644 index 0000000..9601f8f --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/service/InspectionRecordService.java @@ -0,0 +1,37 @@ +package com.ether.pms.mdm.service; + +import com.ether.pms.mdm.entity.InspectionRecord; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * 巡检记录服务接口 + */ +public interface InspectionRecordService { + + InspectionRecord createRecord(InspectionRecord record); + + InspectionRecord updateRecord(UUID id, InspectionRecord record); + + void deleteRecord(UUID id); + + InspectionRecord getRecordById(UUID id); + + List getAllRecords(); + + List getRecordsByEquipment(UUID equipmentId); + + List getRecordsByPlan(UUID planId); + + List getRecordsByInspector(String inspector); + + List getRecordsByStatus(InspectionRecord.CheckStatus status); + + List getRecordsByDateRange(LocalDate startDate, LocalDate endDate); + + List getRecordsByEquipmentAndDateRange(UUID equipmentId, LocalDate startDate, LocalDate endDate); + + InspectionRecord completeRecord(UUID id); +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/MaintenancePlanService.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/MaintenancePlanService.java deleted file mode 100644 index f5d4126..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/service/MaintenancePlanService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.ether.pms.mdm.service; - -import com.ether.pms.mdm.entity.MaintenancePlan; -import java.util.List; -import java.util.UUID; - -public interface MaintenancePlanService { - MaintenancePlan createPlan(MaintenancePlan plan); - MaintenancePlan updatePlan(UUID id, MaintenancePlan plan); - void deactivatePlan(UUID id); - MaintenancePlan getPlanById(UUID id); - List getActivePlansByProject(UUID projectId); - List getPlansByTriggerType(MaintenancePlan.TriggerType triggerType); -} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/MaintenanceTaskService.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/MaintenanceTaskService.java deleted file mode 100644 index 138122d..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/service/MaintenanceTaskService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ether.pms.mdm.service; - -import com.ether.pms.mdm.entity.MaintenancePlan; -import com.ether.pms.mdm.entity.MaintenanceTask; -import java.math.BigDecimal; -import java.util.List; -import java.util.UUID; - -public interface MaintenanceTaskService { - MaintenanceTask getTaskById(UUID id); - List getTasksByAssignee(UUID assignee); - List getTasksByStatus(UUID projectId, MaintenanceTask.Status status); - List getTasksByEquipment(UUID equipmentId); - List getOverdueTasks(); - MaintenanceTask createTask(MaintenanceTask task); - MaintenanceTask acceptTask(UUID taskId, UUID userId); - MaintenanceTask startTask(UUID taskId); - MaintenanceTask completeTask(UUID taskId, BigDecimal laborHours, BigDecimal materialsCost, String remarks); - void cancelTask(UUID taskId); - List generateTasksFromPlan(MaintenancePlan plan); -} \ No newline at end of file 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 8282c18..5b1e609 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,10 +1,14 @@ package com.ether.pms.mdm.service; +import com.ether.pms.auth.repository.UserProjectRepository; import com.ether.pms.mdm.dto.PageResponse; +import com.ether.pms.mdm.dto.ProjectDeleteCheckVO; +import com.ether.pms.mdm.dto.ProjectDeleteStatistics; 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.mdm.repository.SpaceNodeRepository; import com.ether.pms.common.BusinessException; import com.ether.pms.common.ErrorCode; import lombok.RequiredArgsConstructor; @@ -15,6 +19,7 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.time.Year; import java.util.List; import java.util.UUID; @@ -26,6 +31,8 @@ import java.util.stream.Collectors; public class ProjectService { private final ProjectRepository projectRepository; + private final SpaceNodeRepository spaceNodeRepository; + private final UserProjectRepository userProjectRepository; public List findAll() { return projectRepository.findAll(); @@ -232,4 +239,46 @@ public class ProjectService { default -> false; }; } + + /** + * PM-009: 项目删除前检查 + * 检查项目的关联数据情况,如果存在应收未收费用则无法删除 + */ + public ProjectDeleteCheckVO checkProjectDelete(UUID projectId) { + // 验证项目存在 + findById(projectId); + + // 统计各项数据 + long spaceCount = spaceNodeRepository.countByProjectIdAndIsDeletedFalse(projectId); + long memberCount = userProjectRepository.countByProjectId(projectId); + long equipmentCount = projectRepository.countEquipmentsByProjectId(projectId); + + // 账单模块暂未开发,使用占位值 + int billCount = 0; + BigDecimal totalReceivable = BigDecimal.ZERO; + BigDecimal unpaidAmount = BigDecimal.ZERO; + + // 构建统计数据 + ProjectDeleteStatistics statistics = new ProjectDeleteStatistics(); + statistics.setSpaceCount((int) spaceCount); + statistics.setMemberCount((int) memberCount); + statistics.setEquipmentCount((int) equipmentCount); + statistics.setBillCount(billCount); + statistics.setTotalReceivable(totalReceivable); + statistics.setUnpaidAmount(unpaidAmount); + + // 构建返回结果 + ProjectDeleteCheckVO result = new ProjectDeleteCheckVO(); + result.setStatistics(statistics); + + // 如果存在未收金额,则不能删除 + if (unpaidAmount.compareTo(BigDecimal.ZERO) > 0) { + result.setCanDelete(false); + result.setReason(String.format("该项目存在 %d 笔应收未收费用,共计 ¥%s 元", billCount, unpaidAmount.toString())); + } else { + result.setCanDelete(true); + } + + return result; + } } 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 index f75a8f7..dd98b70 100644 --- 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 @@ -1,13 +1,15 @@ package com.ether.pms.mdm.service; +import com.ether.pms.auth.repository.ProjectStaffRepository; import com.ether.pms.mdm.entity.ProjectStatistics; +import com.ether.pms.mdm.entity.SpaceNode; import com.ether.pms.mdm.repository.ProjectStatisticsRepository; +import com.ether.pms.mdm.repository.SpaceNodeRepository; 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; /** @@ -18,84 +20,31 @@ import java.util.UUID; public class ProjectStatisticsService { private final ProjectStatisticsRepository statisticsRepository; + private final SpaceNodeRepository spaceNodeRepository; + private final ProjectStaffRepository projectStaffRepository; /** - * 获取项目统计信息 + * 获取项目统计信息(实时计算) */ public ProjectStatistics getStatistics(UUID projectId) { - return statisticsRepository.findByProjectId(projectId) + ProjectStatistics stats = statisticsRepository.findByProjectId(projectId) .orElseGet(() -> { - ProjectStatistics stats = new ProjectStatistics(); - stats.setProjectId(projectId); - return statisticsRepository.save(stats); + ProjectStatistics newStats = new ProjectStatistics(); + newStats.setProjectId(projectId); + return newStats; }); - } - /** - * 同步成员统计数量 - */ - @Transactional - public void syncMemberStatistics(UUID projectId, int count) { - ProjectStatistics stats = getStatistics(projectId); - stats.setMemberCount(count); - stats.setLastSyncedAt(LocalDateTime.now()); - statisticsRepository.save(stats); - } + // 实时计算各项统计数据 + stats.setMemberCount((int) projectStaffRepository.countByProjectId(projectId)); + stats.setBuildingCount((int) spaceNodeRepository.countByProjectIdAndNodeTypeAndIsDeletedFalse(projectId, SpaceNode.NodeType.BUILDING)); + stats.setRoomCount((int) spaceNodeRepository.countByProjectIdAndNodeTypeAndIsDeletedFalse(projectId, SpaceNode.NodeType.ROOM)); - /** - * 增加成员数量 - */ - @Transactional - public void incrementMemberCount(UUID projectId) { - ProjectStatistics stats = getStatistics(projectId); - stats.setMemberCount(stats.getMemberCount() + 1); - stats.setLastSyncedAt(LocalDateTime.now()); - statisticsRepository.save(stats); - } + // TODO: 业主数和租户数需要从业主表/租户表统计,暂时设为0 + stats.setOwnerCount(0); + stats.setTenantCount(0); - /** - * 减少成员数量 - */ - @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); + return 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 0d31e74..d9dd5c2 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 @@ -3,24 +3,36 @@ package com.ether.pms.mdm.service; 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.SpaceNodeDeleteCheckDTO; import com.ether.pms.mdm.dto.SpaceNodeEquipmentDTO; import com.ether.pms.mdm.dto.SpaceNodeTreeDTO; import com.ether.pms.mdm.dto.SpaceNodeUpdateDTO; +import com.ether.pms.mdm.dto.FloorInfoVO; +import com.ether.pms.mdm.dto.FloorDetailVO; import com.ether.pms.mdm.entity.SpaceNode; import com.ether.pms.mdm.repository.SpaceNodeRepository; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; +import org.springframework.web.multipart.MultipartFile; + +import com.ether.pms.common.ApiResponse; +import com.ether.pms.mdm.dto.EquipmentCreateDTO; + @Service @RequiredArgsConstructor public class SpaceNodeService { @@ -53,8 +65,13 @@ public class SpaceNodeService { return spaceNodeRepository.findByParentIdAndIsDeletedFalseOrderBySortOrderAsc(parentId); } - public List findByParentCode(String parentCode) { - return spaceNodeRepository.findByParentCodeAndIsDeletedFalse(parentCode); + public List findChildrenRecursively(UUID parentId) { + List children = findChildren(parentId); + List allChildren = new ArrayList<>(children); + for (SpaceNode child : children) { + allChildren.addAll(findChildrenRecursively(child.getId())); + } + return allChildren; } public List getTreeByProjectId(UUID projectId) { @@ -90,18 +107,20 @@ public class SpaceNodeService { @Transactional public SpaceNode create(SpaceNodeCreateDTO dto) { - if (spaceNodeRepository.existsByProjectIdAndCode(dto.getProjectId(), dto.getCode())) { - throw new BusinessException(ErrorCode.SPACE_001); - } - 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()); + + if (dto.getNodeType() != null) { + node.setNodeCategory(dto.getNodeType().getCategory()); + node.setNodeType(dto.getNodeType()); + } else if (dto.getSpaceType() != null) { + SpaceNode.NodeType nodeType = SpaceNode.NodeType.valueOf(dto.getSpaceType()); + node.setNodeCategory(nodeType.getCategory()); + node.setNodeType(nodeType); + } node.setUsageType(dto.getUsageType()); node.setSortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0); node.setStatus(dto.getStatus() != null ? dto.getStatus() : "ACTIVE"); @@ -125,12 +144,20 @@ public class SpaceNodeService { if (dto.getParentId() != null) { SpaceNode parent = findById(dto.getParentId()); node.setParentId(parent.getId()); - node.setParentCode(parent.getCode()); node.setLevel(parent.getLevel() + 1); + } else { + node.setLevel(0); + } + + // 先保存获取 ID + node = spaceNodeRepository.save(node); + + // 更新 treePath + if (dto.getParentId() != null) { + SpaceNode parent = findById(dto.getParentId()); 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()); } @@ -229,6 +256,38 @@ public class SpaceNodeService { spaceNodeRepository.save(node); } + public SpaceNodeDeleteCheckDTO checkDeleteInfo(UUID id) { + SpaceNode node = spaceNodeRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new BusinessException(ErrorCode.SPACE_002)); + List children = findChildrenRecursively(id); + Map childTypeCount = children.stream() + .collect(Collectors.groupingBy(c -> c.getNodeType().name(), Collectors.collectingAndThen(Collectors.counting(), Long::intValue))); + int totalDescendant = countDescendants(children); + return new SpaceNodeDeleteCheckDTO(id, node.getName(), children.size(), childTypeCount, totalDescendant); + } + + private int countDescendants(List children) { + int count = children.size(); + for (SpaceNode child : children) { + List grandChildren = findChildren(child.getId()); + count += countDescendants(grandChildren); + } + return count; + } + + @Transactional + public void deleteWithChildren(UUID id) { + SpaceNode node = spaceNodeRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new BusinessException(ErrorCode.SPACE_002)); + List children = findChildrenRecursively(id); + for (SpaceNode child : children) { + child.setIsDeleted(true); + spaceNodeRepository.save(child); + } + node.setIsDeleted(true); + spaceNodeRepository.save(node); + } + // ========== 设备相关方法 ========== /** @@ -281,10 +340,8 @@ public class SpaceNodeService { */ private SpaceNodeEquipmentDTO convertToEquipmentDTO(SpaceNode node) { SpaceNodeEquipmentDTO dto = new SpaceNodeEquipmentDTO(); - // 继承父类属性 dto.setId(node.getId()); dto.setProjectId(node.getProjectId()); - dto.setCode(node.getCode()); dto.setName(node.getName()); dto.setFullName(node.getFullName()); dto.setShortName(node.getShortName()); @@ -292,7 +349,6 @@ public class SpaceNodeService { dto.setNodeType(node.getNodeType()); dto.setUsageType(node.getUsageType()); dto.setParentId(node.getParentId()); - dto.setParentCode(node.getParentCode()); dto.setTreePath(node.getTreePath()); dto.setTreePathName(node.getTreePathName()); dto.setLevel(node.getLevel()); @@ -354,4 +410,324 @@ public class SpaceNodeService { return dto; } + + /** + * 创建设备 + */ + @Transactional + public SpaceNode createEquipment(EquipmentCreateDTO dto) { + SpaceNode node = new SpaceNode(); + node.setProjectId(dto.getProjectId()); + node.setName(dto.getName()); + node.setIsEquipment(true); + node.setNodeCategory(SpaceNode.NodeCategory.FACILITY); + node.setNodeType(SpaceNode.NodeType.EQUIPMENT_ROOM); + + if (dto.getSpaceNodeId() != null) { + SpaceNode parent = findById(dto.getSpaceNodeId()); + node.setParentId(parent.getId()); + node.setLevel(parent.getLevel() + 1); + node.setTreePath(parent.getTreePath() + "." + node.getId()); + node.setTreePathName(parent.getTreePathName() + "/" + node.getName()); + } + + node.setDesignLifeYears(dto.getDesignLifeYears()); + node.setRatedPower(dto.getRatedPower()); + node.setRatedVoltage(dto.getRatedVoltage()); + node.setRatedCurrent(dto.getRatedCurrent()); + node.setMaintenanceVendor(dto.getMaintenanceVendor()); + node.setMaintenanceVendorContact(dto.getMaintenanceVendorContact()); + node.setMaintenanceVendorPhone(dto.getMaintenanceVendorPhone()); + node.setMaintenanceContractNo(dto.getMaintenanceContractNo()); + node.setMaintenanceContractStart(dto.getMaintenanceContractStart()); + node.setMaintenanceContractEnd(dto.getMaintenanceContractEnd()); + node.setSpecialEquipmentType(dto.getSpecialEquipmentType()); + node.setSpecialEquipmentCert(dto.getSpecialEquipmentCert()); + node.setInspectionCycle(dto.getInspectionCycle()); + node.setNextInspectionDate(dto.getNextInspectionDate()); + node.setLastInspectionDate(dto.getLastInspectionDate()); + node.setLastInspectionResult(dto.getLastInspectionResult()); + node.setEnergyConsumptionStandard(dto.getEnergyConsumptionStandard()); + node.setInstallationEnvironment(dto.getInstallationEnvironment()); + node.setProtectionLevel(dto.getProtectionLevel()); + + return spaceNodeRepository.save(node); + } + + /** + * 批量创建空间节点 + */ + @Transactional + public List batchCreate(List dtoList) { + List createdNodes = new ArrayList<>(); + for (SpaceNodeCreateDTO dto : dtoList) { + SpaceNode node = create(dto); + createdNodes.add(node); + } + return createdNodes; + } + + /** + * 批量创建设备 + */ + @Transactional + public List batchCreateEquipment(List dtoList) { + List createdNodes = new ArrayList<>(); + for (EquipmentCreateDTO dto : dtoList) { + SpaceNode node = createEquipment(dto); + createdNodes.add(node); + } + return createdNodes; + } + + /** + * Excel导入设备 + */ + @Transactional + public ApiResponse importEquipmentFromExcel(MultipartFile file, UUID projectId) { + List successList = new ArrayList<>(); + List failList = new ArrayList<>(); + + try { + // 简单解析CSV格式的导入文件 + String content = new String(file.getBytes(), "UTF-8"); + String[] lines = content.split("\n"); + + for (int i = 1; i < lines.length; i++) { + String line = lines[i].trim(); + if (line.isEmpty()) continue; + + try { + String[] fields = line.split(","); + if (fields.length < 2) { + failList.add("第" + (i + 1) + "行: 数据格式错误"); + continue; + } + + EquipmentCreateDTO dto = new EquipmentCreateDTO(); + dto.setName(fields[0].replace("\"", "").trim()); + dto.setProjectId(projectId); // TODO: 需要传入正确的projectId + + if (fields.length > 1 && !fields[1].replace("\"", "").trim().isEmpty()) { + dto.setSpaceNodeId(UUID.fromString(fields[1].replace("\"", "").trim())); + } + if (fields.length > 2 && !fields[2].replace("\"", "").trim().isEmpty()) { + dto.setRatedPower(new BigDecimal(fields[2].replace("\"", "").trim())); + } + if (fields.length > 3 && !fields[3].replace("\"", "").trim().isEmpty()) { + dto.setRatedVoltage(new BigDecimal(fields[3].replace("\"", "").trim())); + } + if (fields.length > 4 && !fields[4].replace("\"", "").trim().isEmpty()) { + dto.setRatedCurrent(new BigDecimal(fields[4].replace("\"", "").trim())); + } + if (fields.length > 5 && !fields[5].replace("\"", "").trim().isEmpty()) { + dto.setDesignLifeYears(Integer.parseInt(fields[5].replace("\"", "").trim())); + } + if (fields.length > 6 && !fields[6].replace("\"", "").trim().isEmpty()) { + dto.setMaintenanceVendor(fields[6].replace("\"", "").trim()); + } + if (fields.length > 7 && !fields[7].replace("\"", "").trim().isEmpty()) { + dto.setMaintenanceVendorPhone(fields[7].replace("\"", "").trim()); + } + if (fields.length > 8 && !fields[8].replace("\"", "").trim().isEmpty()) { + dto.setSpecialEquipmentType(fields[8].replace("\"", "").trim()); + } + if (fields.length > 9 && !fields[9].replace("\"", "").trim().isEmpty()) { + dto.setInspectionCycle(Integer.parseInt(fields[9].replace("\"", "").trim())); + } + + createEquipment(dto); + successList.add(dto.getName()); + } catch (Exception e) { + failList.add("第" + (i + 1) + "行: " + e.getMessage()); + } + } + } catch (Exception e) { + throw new BusinessException(ErrorCode.BAD_REQUEST, "文件解析失败: " + e.getMessage()); + } + + return ApiResponse.success(new ImportResult(successList.size(), failList.size(), failList)); + } + + @Data + public static class ImportResult { + private int successCount; + private int failCount; + private List failDetails; + + public ImportResult(int successCount, int failCount, List failDetails) { + this.successCount = successCount; + this.failCount = failCount; + this.failDetails = failDetails; + } + } + + // ========== 楼层信息相关方法 ========== + + /** + * 获取楼栋楼层信息 + */ + public FloorInfoVO getBuildingFloorInfo(String buildingIdStr) { + UUID buildingId = UUID.fromString(buildingIdStr); + SpaceNode building = findById(buildingId); + + // 必须是楼栋类型 + if (building.getNodeType() != SpaceNode.NodeType.BUILDING) { + throw new BusinessException(ErrorCode.SPACE_001, "指定的节点不是楼栋类型"); + } + + FloorInfoVO floorInfoVO = new FloorInfoVO(); + floorInfoVO.setBuildingId(buildingIdStr); + floorInfoVO.setBuildingName(building.getName()); + + // 尝试从 attributes 中读取楼层配置 + BuildingFloorConfig config = parseBuildingFloorConfig(building.getAttributes()); + + // 查询该楼栋下所有房间和商铺 + List roomAndShops = spaceNodeRepository.findByParentIdAndNodeTypeInAndIsDeletedFalse( + buildingId, List.of(SpaceNode.NodeType.ROOM, SpaceNode.NodeType.SHOP)); + + // 按楼层号分组统计 + Map> floorGrouped = roomAndShops.stream() + .filter(node -> node.getFloorNumber() != null) + .collect(Collectors.groupingBy(SpaceNode::getFloorNumber)); + + // 确定总楼层数和地下楼层数 + int totalFloors = 0; + int undergroundFloors = 0; + + if (!floorGrouped.isEmpty()) { + // 地上楼层:floorNumber >= 1 + int maxAboveGround = floorGrouped.keySet().stream() + .filter(f -> f >= 1) + .max(Integer::compareTo) + .orElse(0); + totalFloors = maxAboveGround; + + // 地下楼层:floorNumber < 0 + int maxUnderground = floorGrouped.keySet().stream() + .filter(f -> f < 0) + .min(Integer::compareTo) + .orElse(0); + // 地下楼层数 = |最小负楼层号|,如 -1 则 1层,-2 则 2层 + if (maxUnderground < 0) { + undergroundFloors = Math.abs(maxUnderground); + } + } else if (config != null) { + // 没有房间数据时使用配置 + totalFloors = config.getTotalFloors() != null ? config.getTotalFloors() : 0; + undergroundFloors = config.getUndergroundFloors() != null ? config.getUndergroundFloors() : 0; + } + + floorInfoVO.setTotalFloors(totalFloors); + floorInfoVO.setUndergroundFloors(undergroundFloors); + + // 构建楼层详情列表 + List floors = new ArrayList<>(); + + // 添加配置的楼层信息 + if (config != null && config.getFloorConfig() != null) { + for (Map.Entry entry : config.getFloorConfig().entrySet()) { + Integer floorNum = Integer.parseInt(entry.getKey()); + FloorConfigItem configItem = entry.getValue(); + + FloorDetailVO detail = new FloorDetailVO(); + detail.setFloorNumber(floorNum); + detail.setHasRooms(configItem.getHasRooms()); + detail.setHasShop(configItem.getHasShop()); + detail.setRemark(configItem.getRemark()); + + // 如果有实际房间数据,统计房间数 + if (floorGrouped.containsKey(floorNum)) { + List nodes = floorGrouped.get(floorNum); + detail.setRoomCount(nodes.size()); + // 根据实际数据更新 hasRooms/hasShop + detail.setHasRooms(nodes.stream().anyMatch(n -> n.getNodeType() == SpaceNode.NodeType.ROOM)); + detail.setHasShop(nodes.stream().anyMatch(n -> n.getNodeType() == SpaceNode.NodeType.SHOP)); + } else { + detail.setRoomCount(0); + } + + floors.add(detail); + } + } + + // 如果没有配置信息,从实际数据构建 + if (floors.isEmpty()) { + // 添加地上楼层 + for (int i = 1; i <= totalFloors; i++) { + FloorDetailVO detail = new FloorDetailVO(); + detail.setFloorNumber(i); + if (floorGrouped.containsKey(i)) { + List nodes = floorGrouped.get(i); + detail.setRoomCount(nodes.size()); + detail.setHasRooms(nodes.stream().anyMatch(n -> n.getNodeType() == SpaceNode.NodeType.ROOM)); + detail.setHasShop(nodes.stream().anyMatch(n -> n.getNodeType() == SpaceNode.NodeType.SHOP)); + } else { + detail.setRoomCount(0); + detail.setHasRooms(false); + detail.setHasShop(false); + } + floors.add(detail); + } + + // 添加地下楼层 + for (int i = -undergroundFloors; i < 0; i++) { + FloorDetailVO detail = new FloorDetailVO(); + detail.setFloorNumber(i); + if (floorGrouped.containsKey(i)) { + List nodes = floorGrouped.get(i); + detail.setRoomCount(nodes.size()); + detail.setHasRooms(nodes.stream().anyMatch(n -> n.getNodeType() == SpaceNode.NodeType.ROOM)); + detail.setHasShop(nodes.stream().anyMatch(n -> n.getNodeType() == SpaceNode.NodeType.SHOP)); + } else { + detail.setRoomCount(0); + detail.setHasRooms(false); + detail.setHasShop(false); + } + floors.add(detail); + } + } + + // 按楼层号排序 + floors.sort((a, b) -> a.getFloorNumber().compareTo(b.getFloorNumber())); + + floorInfoVO.setFloors(floors); + return floorInfoVO; + } + + /** + * 解析楼栋楼层配置JSON + */ + private BuildingFloorConfig parseBuildingFloorConfig(String attributes) { + if (attributes == null || attributes.isEmpty()) { + return null; + } + try { + return objectMapper.readValue(attributes, BuildingFloorConfig.class); + } catch (JsonProcessingException e) { + return null; + } + } + + /** + * 楼栋楼层配置(从JSON解析) + */ + @Data + public static class BuildingFloorConfig { + private Integer totalFloors; + private Integer undergroundFloors; + private Map floorConfig; + } + + /** + * 楼层配置项 + */ + @Data + public static class FloorConfigItem { + private Boolean hasRooms; + private Boolean hasShop; + private String remark; + } + } diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/InspectionItemServiceImpl.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/InspectionItemServiceImpl.java new file mode 100644 index 0000000..2e1a0f1 --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/InspectionItemServiceImpl.java @@ -0,0 +1,110 @@ +package com.ether.pms.mdm.service.impl; + +import com.ether.pms.common.BusinessException; +import com.ether.pms.common.ErrorCode; +import com.ether.pms.mdm.entity.InspectionItem; +import com.ether.pms.mdm.repository.InspectionItemRepository; +import com.ether.pms.mdm.service.InspectionItemService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +/** + * 巡检标准项服务实现 + */ +@Service +@RequiredArgsConstructor +public class InspectionItemServiceImpl implements InspectionItemService { + + private final InspectionItemRepository inspectionItemRepository; + + @Override + @Transactional + public InspectionItem createItem(InspectionItem item) { + if (item.getStatus() == null) { + item.setStatus(InspectionItem.Status.ACTIVE); + } + if (item.getIsRequired() == null) { + item.setIsRequired(true); + } + return inspectionItemRepository.save(item); + } + + @Override + @Transactional + public InspectionItem updateItem(UUID id, InspectionItem item) { + InspectionItem existing = getItemById(id); + + if (item.getItemName() != null) { + existing.setItemName(item.getItemName()); + } + if (item.getEquipmentType() != null) { + existing.setEquipmentType(item.getEquipmentType()); + } + if (item.getSystemType() != null) { + existing.setSystemType(item.getSystemType()); + } + if (item.getCheckMethod() != null) { + existing.setCheckMethod(item.getCheckMethod()); + } + if (item.getStandardValue() != null) { + existing.setStandardValue(item.getStandardValue()); + } + if (item.getIsRequired() != null) { + existing.setIsRequired(item.getIsRequired()); + } + if (item.getRemark() != null) { + existing.setRemark(item.getRemark()); + } + if (item.getSortOrder() != null) { + existing.setSortOrder(item.getSortOrder()); + } + if (item.getStatus() != null) { + existing.setStatus(item.getStatus()); + } + + return inspectionItemRepository.save(existing); + } + + @Override + @Transactional + public void deleteItem(UUID id) { + InspectionItem item = getItemById(id); + item.setStatus(InspectionItem.Status.INACTIVE); + inspectionItemRepository.save(item); + } + + @Override + public InspectionItem getItemById(UUID id) { + return inspectionItemRepository.findById(id) + .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "巡检标准项不存在")); + } + + @Override + public List getAllItems() { + return inspectionItemRepository.findAll(); + } + + @Override + public List getItemsByEquipmentType(String equipmentType) { + return inspectionItemRepository.findByEquipmentTypeAndStatus(equipmentType, InspectionItem.Status.ACTIVE); + } + + @Override + public List getItemsBySystemType(String systemType) { + return inspectionItemRepository.findBySystemTypeAndStatus(systemType, InspectionItem.Status.ACTIVE); + } + + @Override + public List getActiveItems() { + return inspectionItemRepository.findAllActiveOrderBySortOrder(); + } + + @Override + public List getItemsByEquipmentTypeAndSystemType(String equipmentType, String systemType) { + return inspectionItemRepository.findByEquipmentTypeAndSystemType(equipmentType, systemType); + } +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/InspectionRecordServiceImpl.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/InspectionRecordServiceImpl.java new file mode 100644 index 0000000..55b7b0e --- /dev/null +++ b/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/InspectionRecordServiceImpl.java @@ -0,0 +1,132 @@ +package com.ether.pms.mdm.service.impl; + +import com.ether.pms.common.BusinessException; +import com.ether.pms.common.ErrorCode; +import com.ether.pms.mdm.entity.InspectionRecord; +import com.ether.pms.mdm.repository.InspectionRecordRepository; +import com.ether.pms.mdm.service.InspectionRecordService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * 巡检记录服务实现 + */ +@Service +@RequiredArgsConstructor +public class InspectionRecordServiceImpl implements InspectionRecordService { + + private final InspectionRecordRepository inspectionRecordRepository; + + @Override + @Transactional + public InspectionRecord createRecord(InspectionRecord record) { + if (record.getStatus() == null) { + record.setStatus(InspectionRecord.CheckStatus.NORMAL); + } + if (record.getCompleted() == null) { + record.setCompleted(false); + } + return inspectionRecordRepository.save(record); + } + + @Override + @Transactional + public InspectionRecord updateRecord(UUID id, InspectionRecord record) { + InspectionRecord existing = getRecordById(id); + + if (record.getInspectionDate() != null) { + existing.setInspectionDate(record.getInspectionDate()); + } + if (record.getInspector() != null) { + existing.setInspector(record.getInspector()); + } + if (record.getStatus() != null) { + existing.setStatus(record.getStatus()); + } + if (record.getCheckInTime() != null) { + existing.setCheckInTime(record.getCheckInTime()); + } + if (record.getCheckInLocation() != null) { + existing.setCheckInLocation(record.getCheckInLocation()); + } + if (record.getCheckInPhoto() != null) { + existing.setCheckInPhoto(record.getCheckInPhoto()); + } + if (record.getItems() != null) { + existing.setItems(record.getItems()); + } + if (record.getProblems() != null) { + existing.setProblems(record.getProblems()); + } + if (record.getCompleted() != null) { + existing.setCompleted(record.getCompleted()); + } + + return inspectionRecordRepository.save(existing); + } + + @Override + @Transactional + public void deleteRecord(UUID id) { + InspectionRecord record = getRecordById(id); + inspectionRecordRepository.delete(record); + } + + @Override + public InspectionRecord getRecordById(UUID id) { + return inspectionRecordRepository.findById(id) + .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "巡检记录不存在")); + } + + @Override + public List getAllRecords() { + return inspectionRecordRepository.findAll(); + } + + @Override + public List getRecordsByEquipment(UUID equipmentId) { + return inspectionRecordRepository.findByEquipmentIdOrderByInspectionDateDesc(equipmentId); + } + + @Override + public List getRecordsByPlan(UUID planId) { + return inspectionRecordRepository.findByPlanId(planId); + } + + @Override + public List getRecordsByInspector(String inspector) { + return inspectionRecordRepository.findByInspector(inspector); + } + + @Override + public List getRecordsByStatus(InspectionRecord.CheckStatus status) { + return inspectionRecordRepository.findByStatus(status); + } + + @Override + public List getRecordsByDateRange(LocalDate startDate, LocalDate endDate) { + return inspectionRecordRepository.findByDateRange(startDate, endDate); + } + + @Override + public List getRecordsByEquipmentAndDateRange(UUID equipmentId, LocalDate startDate, LocalDate endDate) { + return inspectionRecordRepository.findByEquipmentIdAndInspectionDateBetween(equipmentId, startDate, endDate); + } + + @Override + @Transactional + public InspectionRecord completeRecord(UUID id) { + InspectionRecord record = getRecordById(id); + + record.setCompleted(true); + record.setCompletedTime(LocalDateTime.now()); + + return inspectionRecordRepository.save(record); + } +} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/MaintenancePlanServiceImpl.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/MaintenancePlanServiceImpl.java deleted file mode 100644 index cb59de2..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/MaintenancePlanServiceImpl.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.ether.pms.mdm.service.impl; - -import com.ether.pms.common.BusinessException; -import com.ether.pms.common.ErrorCode; -import com.ether.pms.mdm.entity.MaintenancePlan; -import com.ether.pms.mdm.repository.MaintenancePlanRepository; -import com.ether.pms.mdm.service.MaintenancePlanService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class MaintenancePlanServiceImpl implements MaintenancePlanService { - - private final MaintenancePlanRepository maintenancePlanRepository; - private static final DateTimeFormatter CODE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); - - @Override - @Transactional - public MaintenancePlan createPlan(MaintenancePlan plan) { - // 生成计划编码 MP + 时间戳 - String planCode = generatePlanCode(); - plan.setPlanCode(planCode); - - // 设置默认状态为ACTIVE - if (plan.getStatus() == null) { - plan.setStatus(MaintenancePlan.Status.ACTIVE); - } - - return maintenancePlanRepository.save(plan); - } - - @Override - @Transactional - public MaintenancePlan updatePlan(UUID id, MaintenancePlan plan) { - MaintenancePlan existing = getPlanById(id); - - // 状态转换校验:只有ACTIVE状态才能更新 - if (existing.getStatus() != MaintenancePlan.Status.ACTIVE) { - throw new BusinessException(6001, "只有ACTIVE状态的计划才能更新"); - } - - // 更新字段 - if (plan.getPlanName() != null) { - existing.setPlanName(plan.getPlanName()); - } - if (plan.getEquipmentType() != null) { - existing.setEquipmentType(plan.getEquipmentType()); - } - if (plan.getTriggerType() != null) { - existing.setTriggerType(plan.getTriggerType()); - } - if (plan.getTriggerValue() != null) { - existing.setTriggerValue(plan.getTriggerValue()); - } - if (plan.getTriggerUnit() != null) { - existing.setTriggerUnit(plan.getTriggerUnit()); - } - if (plan.getMaintenanceItems() != null) { - existing.setMaintenanceItems(plan.getMaintenanceItems()); - } - if (plan.getEstimatedDuration() != null) { - existing.setEstimatedDuration(plan.getEstimatedDuration()); - } - if (plan.getAssignedTo() != null) { - existing.setAssignedTo(plan.getAssignedTo()); - } - if (plan.getSlaResponseHours() != null) { - existing.setSlaResponseHours(plan.getSlaResponseHours()); - } - if (plan.getSlaCompleteHours() != null) { - existing.setSlaCompleteHours(plan.getSlaCompleteHours()); - } - - return maintenancePlanRepository.save(existing); - } - - @Override - @Transactional - public void deactivatePlan(UUID id) { - MaintenancePlan plan = getPlanById(id); - - // 状态转换校验:只有ACTIVE状态才能停用 - if (plan.getStatus() != MaintenancePlan.Status.ACTIVE) { - throw new BusinessException(6002, "只有ACTIVE状态的计划才能停用"); - } - - plan.setStatus(MaintenancePlan.Status.INACTIVE); - maintenancePlanRepository.save(plan); - } - - @Override - public MaintenancePlan getPlanById(UUID id) { - return maintenancePlanRepository.findById(id) - .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "维保计划不存在")); - } - - @Override - public List getActivePlansByProject(UUID projectId) { - return maintenancePlanRepository.findByProjectIdAndStatus(projectId, MaintenancePlan.Status.ACTIVE); - } - - @Override - public List getPlansByTriggerType(MaintenancePlan.TriggerType triggerType) { - return maintenancePlanRepository.findByTriggerType(triggerType); - } - - /** - * 生成计划编码:MP + yyyyMMddHHmmss - */ - private String generatePlanCode() { - String timestamp = LocalDateTime.now().format(CODE_FORMATTER); - String planCode = "MP" + timestamp; - - // 确保编码唯一 - int suffix = 1; - while (maintenancePlanRepository.existsByPlanCode(planCode)) { - planCode = "MP" + timestamp + "_" + suffix; - suffix++; - } - - return planCode; - } -} \ No newline at end of file diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/MaintenanceTaskServiceImpl.java b/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/MaintenanceTaskServiceImpl.java deleted file mode 100644 index 2676fd1..0000000 --- a/module-mdm/src/main/java/com/ether/pms/mdm/service/impl/MaintenanceTaskServiceImpl.java +++ /dev/null @@ -1,200 +0,0 @@ -package com.ether.pms.mdm.service.impl; - -import com.ether.pms.common.BusinessException; -import com.ether.pms.mdm.entity.MaintenancePlan; -import com.ether.pms.mdm.entity.MaintenanceTask; -import com.ether.pms.mdm.repository.MaintenanceTaskRepository; -import com.ether.pms.mdm.service.MaintenanceTaskService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class MaintenanceTaskServiceImpl implements MaintenanceTaskService { - - private final MaintenanceTaskRepository maintenanceTaskRepository; - private static final DateTimeFormatter CODE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); - - @Override - public MaintenanceTask getTaskById(UUID id) { - return maintenanceTaskRepository.findById(id) - .orElseThrow(() -> new BusinessException(6003, "维保任务不存在")); - } - - @Override - public List getTasksByAssignee(UUID assignee) { - return maintenanceTaskRepository.findByAssignedToAndStatus(assignee, MaintenanceTask.Status.PENDING); - } - - @Override - public List getTasksByStatus(UUID projectId, MaintenanceTask.Status status) { - return maintenanceTaskRepository.findByProjectIdAndStatus(projectId, status); - } - - @Override - public List getTasksByEquipment(UUID equipmentId) { - return maintenanceTaskRepository.findByEquipmentIdAndStatusNot(equipmentId, MaintenanceTask.Status.CANCELLED); - } - - @Override - public List getOverdueTasks() { - return maintenanceTaskRepository.findOverdueTasks(LocalDateTime.now()); - } - - @Override - @Transactional - public MaintenanceTask createTask(MaintenanceTask task) { - // 生成任务编码 MT + 时间戳 - String taskCode = generateTaskCode(); - task.setTaskCode(taskCode); - - // 设置默认状态为PENDING - if (task.getStatus() == null) { - task.setStatus(MaintenanceTask.Status.PENDING); - } - - // 设置默认任务类型为预防性维护 - if (task.getTaskType() == null) { - task.setTaskType(MaintenanceTask.TaskType.PREVENTIVE); - } - - return maintenanceTaskRepository.save(task); - } - - @Override - @Transactional - public MaintenanceTask acceptTask(UUID taskId, UUID userId) { - MaintenanceTask task = getTaskById(taskId); - - // 状态校验:只有PENDING状态才能接受 - if (task.getStatus() != MaintenanceTask.Status.PENDING) { - throw new BusinessException(6004, "只有PENDING状态的任务才能接受"); - } - - task.setStatus(MaintenanceTask.Status.ACCEPTED); - task.setAssignedTo(userId); - - return maintenanceTaskRepository.save(task); - } - - @Override - @Transactional - public MaintenanceTask startTask(UUID taskId) { - MaintenanceTask task = getTaskById(taskId); - - // 状态校验:只有ACCEPTED状态才能开始执行 - if (task.getStatus() != MaintenanceTask.Status.ACCEPTED) { - throw new BusinessException(6005, "只有ACCEPTED状态的任务才能开始执行"); - } - - task.setStatus(MaintenanceTask.Status.IN_PROGRESS); - task.setActualStartDate(LocalDateTime.now()); - - return maintenanceTaskRepository.save(task); - } - - @Override - @Transactional - public MaintenanceTask completeTask(UUID taskId, BigDecimal laborHours, BigDecimal materialsCost, String remarks) { - MaintenanceTask task = getTaskById(taskId); - - // 状态校验:只有IN_PROGRESS状态才能完成 - if (task.getStatus() != MaintenanceTask.Status.IN_PROGRESS) { - throw new BusinessException(6006, "只有IN_PROGRESS状态的任务才能完成"); - } - - task.setStatus(MaintenanceTask.Status.COMPLETED); - task.setActualEndDate(LocalDateTime.now()); - - if (laborHours != null) { - task.setLaborHours(laborHours); - } - if (materialsCost != null) { - task.setMaterialsCost(materialsCost); - } - if (remarks != null) { - task.setRemarks(remarks); - } - - return maintenanceTaskRepository.save(task); - } - - @Override - @Transactional - public void cancelTask(UUID taskId) { - MaintenanceTask task = getTaskById(taskId); - - // 状态校验:PENDING、ACCEPTED、IN_PROGRESS状态都可以取消 - List cancellableStatuses = Arrays.asList( - MaintenanceTask.Status.PENDING, - MaintenanceTask.Status.ACCEPTED, - MaintenanceTask.Status.IN_PROGRESS - ); - - if (!cancellableStatuses.contains(task.getStatus())) { - throw new BusinessException(6007, "只有PENDING、ACCEPTED或IN_PROGRESS状态的任务才能取消"); - } - - task.setStatus(MaintenanceTask.Status.CANCELLED); - maintenanceTaskRepository.save(task); - } - - @Override - @Transactional - public List generateTasksFromPlan(MaintenancePlan plan) { - List generatedTasks = new ArrayList<>(); - - // 根据触发类型生成相应数量的任务 - int taskCount = 1; // 默认生成1个任务 - if (plan.getTriggerValue() != null && plan.getTriggerValue() > 0) { - taskCount = plan.getTriggerValue(); - } - - for (int i = 0; i < taskCount; i++) { - MaintenanceTask task = new MaintenanceTask(); - task.setProjectId(plan.getProjectId()); - task.setPlanId(plan.getId()); - task.setTriggerType(plan.getTriggerType()); - task.setMaintenanceItems(plan.getMaintenanceItems()); - task.setAssignedTo(plan.getAssignedTo()); - task.setStatus(MaintenanceTask.Status.PENDING); - task.setTaskType(MaintenanceTask.TaskType.PREVENTIVE); - - // 设置计划执行时间(如果有) - if (plan.getEstimatedDuration() != null) { - task.setScheduledDate(LocalDateTime.now().plusDays(i * (long) plan.getEstimatedDuration())); - } - - MaintenanceTask savedTask = createTask(task); - generatedTasks.add(savedTask); - } - - return generatedTasks; - } - - /** - * 生成任务编码:MT + yyyyMMddHHmmss - */ - private String generateTaskCode() { - String timestamp = LocalDateTime.now().format(CODE_FORMATTER); - String taskCode = "MT" + timestamp; - - // 确保编码唯一 - int suffix = 1; - while (maintenanceTaskRepository.existsByTaskCode(taskCode)) { - taskCode = "MT" + timestamp + "_" + suffix; - suffix++; - } - - return taskCode; - } -} \ No newline at end of file diff --git a/module-mdm/src/main/resources/db/migration/V20260325__create_equipment_tables.sql b/module-mdm/src/main/resources/db/migration/V20260325__create_equipment_tables.sql new file mode 100644 index 0000000..85312d5 --- /dev/null +++ b/module-mdm/src/main/resources/db/migration/V20260325__create_equipment_tables.sql @@ -0,0 +1,262 @@ +-- ============================================ +-- 设施设备模块数据库结构变更 +-- 执行时间: 2026-03-25 +-- 说明: 创建设备独立表,支持多类型扩展和归属管理 +-- ============================================ + +-- 1. 设备主表 +CREATE TABLE mdm_equipment ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID, + space_node_id UUID, + + -- 基础信息 + equipment_code VARCHAR(50) UNIQUE NOT NULL, + equipment_name VARCHAR(100) NOT NULL, + equipment_type VARCHAR(30) NOT NULL, + equipment_category VARCHAR(50), + + -- 归属信息 + ownership_type VARCHAR(20) NOT NULL DEFAULT 'PROJECT', + owning_entity_id UUID, + owning_entity_name VARCHAR(100), + + -- 资产信息 + asset_code VARCHAR(50), + serial_number VARCHAR(100), + model VARCHAR(100), + manufacturer VARCHAR(100), + supplier VARCHAR(100), + + -- 状态 + status VARCHAR(20) DEFAULT 'ACTIVE', + operation_status VARCHAR(20), + + -- 位置 + installation_location VARCHAR(200), + installation_date DATE, + + -- 设计参数 + design_life_years INT, + rated_power DECIMAL(10, 2), + rated_voltage VARCHAR(20), + rated_current DECIMAL(10, 2), + + -- 维保信息 + maintenance_vendor VARCHAR(100), + maintenance_vendor_contact VARCHAR(50), + maintenance_vendor_phone VARCHAR(20), + maintenance_contract_no VARCHAR(50), + maintenance_contract_start DATE, + maintenance_contract_end DATE, + + -- 能耗 + energy_consumption_standard DECIMAL(12, 2), + + -- 检验信息 + inspection_cycle INT, + next_inspection_date DATE, + last_inspection_date DATE, + last_inspection_result VARCHAR(20), + special_equipment_type VARCHAR(50), + special_equipment_cert VARCHAR(100), + + -- 公共扩展字段(JSON格式存储其他属性) + attributes TEXT, + + -- 审计字段 + remarks TEXT, + is_deleted BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by UUID, + updated_by UUID +); + +-- 2. 归属主体表 +CREATE TABLE mdm_ownership_entity ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + entity_type VARCHAR(20) NOT NULL, + entity_name VARCHAR(100) NOT NULL, + entity_code VARCHAR(50), + + -- 联系信息 + contact_person VARCHAR(50), + contact_phone VARCHAR(20), + contact_address VARCHAR(255), + + -- 公司信息 + business_license VARCHAR(50), + legal_representative VARCHAR(50), + + -- 租赁信息 + contract_no VARCHAR(50), + contract_start_date DATE, + contract_end_date DATE, + rental_fee DECIMAL(12, 2), + + -- 审计 + status VARCHAR(20) DEFAULT 'ACTIVE', + is_deleted BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 3. 电梯扩展表 +CREATE TABLE mdm_equipment_elevator ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + equipment_id UUID NOT NULL UNIQUE, + elevator_type VARCHAR(30), + elevator_model VARCHAR(50), + load_capacity INT, + speed DECIMAL(10, 2), + floor_count INT, + shaft_dimensions VARCHAR(50), + pit_depth DECIMAL(10, 2), + overhead_height DECIMAL(10, 2), + registration_no VARCHAR(50), + inspection_certificate VARCHAR(100), + next_inspection_date DATE, + energy_consumption DECIMAL(12, 2), + maintenance_level VARCHAR(20), + rescue_plan TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 4. 暖通空调扩展表 +CREATE TABLE mdm_equipment_hvac ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + equipment_id UUID NOT NULL UNIQUE, + hvac_type VARCHAR(30), + cooling_capacity DECIMAL(12, 2), + heating_capacity DECIMAL(12, 2), + air_flow DECIMAL(12, 2), + refrigerant_type VARCHAR(30), + refrigerant_charge DECIMAL(10, 2), + energy_efficiency_ratio DECIMAL(10, 2), + coefficient_of_performance DECIMAL(10, 2), + installation_date DATE, + warranty_expire_date DATE, + filter_replacement_cycle INT, + last_filter_replacement DATE, + duct_type VARCHAR(30), + duct_dimensions VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 5. 能源计量扩展表 +CREATE TABLE mdm_equipment_energy ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + equipment_id UUID NOT NULL UNIQUE, + meter_type VARCHAR(30), + energy_type VARCHAR(30), + meter_model VARCHAR(50), + meter_specification VARCHAR(50), + meter_constant DECIMAL(10, 4), + accuracy_class VARCHAR(10), + reading_type VARCHAR(20), + last_reading_date DATE, + last_reading_value DECIMAL(12, 2), + current_reading_value DECIMAL(12, 2), + unit_price DECIMAL(10, 4), + billing_type VARCHAR(20), + communication_type VARCHAR(30), + communication_address VARCHAR(50), + verification_cycle INT, + next_verification_date DATE, + verification_certificate VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 6. 消防扩展表 +CREATE TABLE mdm_equipment_fire ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + equipment_id UUID NOT NULL UNIQUE, + fire_equipment_type VARCHAR(30), + installation_area DECIMAL(10, 2), + installation_height DECIMAL(10, 2), + detection_range DECIMAL(10, 2), + system_type VARCHAR(30), + zone_number VARCHAR(20), + loop_number VARCHAR(20), + linkage_enabled BOOLEAN, + linkage_action VARCHAR(100), + inspection_cycle INT, + last_inspection_date DATE, + next_inspection_date DATE, + inspection_result VARCHAR(20), + special_requirement TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ============================================ +-- 索引 +-- ============================================ + +-- 设备表索引 +CREATE INDEX idx_equipment_project ON mdm_equipment(project_id); +CREATE INDEX idx_equipment_space ON mdm_equipment(space_node_id); +CREATE INDEX idx_equipment_type ON mdm_equipment(equipment_type); +CREATE INDEX idx_equipment_ownership ON mdm_equipment(ownership_type); +CREATE INDEX idx_equipment_code ON mdm_equipment(equipment_code); +CREATE INDEX idx_equipment_status ON mdm_equipment(status); + +-- 归属主体表索引 +CREATE INDEX idx_ownership_entity_type ON mdm_ownership_entity(entity_type); +CREATE INDEX idx_ownership_entity_code ON mdm_ownership_entity(entity_code); + +-- 扩展表索引 +CREATE INDEX idx_elevator_equipment ON mdm_equipment_elevator(equipment_id); +CREATE INDEX idx_hvac_equipment ON mdm_equipment_hvac(equipment_id); +CREATE INDEX idx_energy_equipment ON mdm_equipment_energy(equipment_id); +CREATE INDEX idx_fire_equipment ON mdm_equipment_fire(equipment_id); + +-- ============================================ +-- 外键约束 +-- ============================================ + +ALTER TABLE mdm_equipment + ADD CONSTRAINT fk_equipment_project + FOREIGN KEY (project_id) REFERENCES mdm_project(id); + +ALTER TABLE mdm_equipment + ADD CONSTRAINT fk_equipment_space + FOREIGN KEY (space_node_id) REFERENCES mdm_space_node(id); + +ALTER TABLE mdm_equipment + ADD CONSTRAINT fk_equipment_ownership + FOREIGN KEY (owning_entity_id) REFERENCES mdm_ownership_entity(id); + +ALTER TABLE mdm_equipment_elevator + ADD CONSTRAINT fk_elevator_equipment + FOREIGN KEY (equipment_id) REFERENCES mdm_equipment(id) ON DELETE CASCADE; + +ALTER TABLE mdm_equipment_hvac + ADD CONSTRAINT fk_hvac_equipment + FOREIGN KEY (equipment_id) REFERENCES mdm_equipment(id) ON DELETE CASCADE; + +ALTER TABLE mdm_equipment_energy + ADD CONSTRAINT fk_energy_equipment + FOREIGN KEY (equipment_id) REFERENCES mdm_equipment(id) ON DELETE CASCADE; + +ALTER TABLE mdm_equipment_fire + ADD CONSTRAINT fk_fire_equipment + FOREIGN KEY (equipment_id) REFERENCES mdm_equipment(id) ON DELETE CASCADE; + +-- ============================================ +-- 注释 +-- ============================================ + +COMMENT ON TABLE mdm_equipment IS '设备主表'; +COMMENT ON TABLE mdm_ownership_entity IS '设备归属主体表'; +COMMENT ON TABLE mdm_equipment_elevator IS '电梯设备扩展表'; +COMMENT ON TABLE mdm_equipment_hvac IS '暖通空调设备扩展表'; +COMMENT ON TABLE mdm_equipment_energy IS '能源计量设备扩展表'; +COMMENT ON TABLE mdm_equipment_fire IS '消防设备扩展表'; + +COMMENT ON COLUMN mdm_equipment.ownership_type IS '归属类型: PROJECT-项目自有, COMPANY-公司统筹, OWNER-业主自置, RENTAL-租赁设备'; +COMMENT ON COLUMN mdm_equipment.equipment_type IS '设备大类: ELEVATOR-电梯, HVAC-暖通, FIRE-消防, PLUMBING-给排水, ELECTRICAL-电气, ENERGY-能源计量, SECURITY-弱电, OTHER-其他'; diff --git a/module-mdm/src/main/resources/db/migration/V20260326__migrate_equipment_data.sql b/module-mdm/src/main/resources/db/migration/V20260326__migrate_equipment_data.sql new file mode 100644 index 0000000..7d0fe42 --- /dev/null +++ b/module-mdm/src/main/resources/db/migration/V20260326__migrate_equipment_data.sql @@ -0,0 +1,151 @@ +-- ============================================ +-- 数据迁移脚本:从 SpaceNode 迁移设备到 mdm_equipment +-- 执行时间: 2026-03-25 +-- 说明: 将 space_node 表中 is_equipment=true 的记录迁移到新设备表 +-- 注意: 执行前请先备份数据! +-- ============================================ + +-- 1. 创建临时表记录迁移日志 +CREATE TABLE IF NOT EXISTS equipment_migration_log ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + old_space_node_id UUID NOT NULL, + new_equipment_id UUID, + migration_status VARCHAR(20), -- SUCCESS, FAILED, SKIPPED + error_message TEXT, + migrated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 2. 迁移函数 +CREATE OR REPLACE FUNCTION migrate_equipment_from_space_node() +RETURNS void AS $$ +DECLARE + space_record RECORD; + new_equipment_id UUID; + migration_count INT := 0; + error_count INT := 0; +BEGIN + -- 遍历所有设备记录 + FOR space_record IN + SELECT id, project_id, name, full_name, design_life_years, + rated_power, rated_voltage, rated_current, + maintenance_vendor, maintenance_vendor_contact, maintenance_vendor_phone, + maintenance_contract_no, maintenance_contract_start, maintenance_contract_end, + installation_environment, protection_level, + inspection_cycle, next_inspection_date, last_inspection_date, last_inspection_result, + special_equipment_type, special_equipment_cert, + created_at, updated_at, created_by, updated_by, status, is_deleted + FROM mdm_space_node + WHERE is_equipment = TRUE + LOOP + BEGIN + -- 生成设备编码 + new_equipment_id := gen_random_uuid(); + + -- 插入设备记录 + INSERT INTO mdm_equipment ( + id, project_id, space_node_id, equipment_code, equipment_name, + equipment_type, equipment_category, + ownership_type, + design_life_years, rated_power, rated_voltage, rated_current, + maintenance_vendor, maintenance_vendor_contact, maintenance_vendor_phone, + maintenance_contract_no, maintenance_contract_start, maintenance_contract_end, + installation_location, + inspection_cycle, next_inspection_date, last_inspection_date, last_inspection_result, + special_equipment_type, special_equipment_cert, + created_at, updated_at, created_by, updated_by, status, is_deleted + ) VALUES ( + new_equipment_id, + space_record.project_id, + space_record.id, -- 安装位置指向原space_node + 'EQC-' || SUBSTRING(new_equipment_id::TEXT, 1, 8), -- 生成设备编码 + space_record.name, + 'OTHER', -- 默认设备类型,需要后续分类 + NULL, + 'PROJECT', -- 默认项目自有 + space_record.design_life_years, + space_record.rated_power, + space_record.rated_voltage, + space_record.rated_current, + space_record.maintenance_vendor, + space_record.maintenance_vendor_contact, + space_record.maintenance_vendor_phone, + space_record.maintenance_contract_no, + space_record.maintenance_contract_start, + space_record.maintenance_contract_end, + space_record.installation_environment, + space_record.inspection_cycle, + space_record.next_inspection_date, + space_record.last_inspection_date, + space_record.last_inspection_result, + space_record.special_equipment_type, + space_record.special_equipment_cert, + space_record.created_at, + space_record.updated_at, + space_record.created_by, + space_record.updated_by, + COALESCE(space_record.status, 'ACTIVE'), + COALESCE(space_record.is_deleted, FALSE) + ); + + -- 记录迁移日志 + INSERT INTO equipment_migration_log (old_space_node_id, new_equipment_id, migration_status) + VALUES (space_record.id, new_equipment_id, 'SUCCESS'); + + migration_count := migration_count + 1; + + EXCEPTION WHEN OTHERS THEN + -- 记录失败日志 + INSERT INTO equipment_migration_log (old_space_node_id, migration_status, error_message) + VALUES (space_record.id, 'FAILED', SQLERRM); + + error_count := error_count + 1; + END; + END LOOP; + + -- 输出迁移结果 + RAISE NOTICE 'Migration completed: % succeeded, % failed', migration_count, error_count; +END; +$$ LANGUAGE plpgsql; + +-- 3. 执行迁移(取消注释以执行) +-- SELECT migrate_equipment_from_space_node(); + +-- 4. 验证迁移结果 +-- SELECT migration_status, COUNT(*) as count FROM equipment_migration_log GROUP BY migration_status; + +-- 5. 查看迁移后的设备数量 +-- SELECT COUNT(*) as total_equipment FROM mdm_equipment; + +-- 6. 回滚函数(如需回滚) +CREATE OR REPLACE FUNCTION rollback_equipment_migration() +RETURNS void AS $$ +DECLARE + log_record RECORD; +BEGIN + FOR log_record IN + SELECT old_space_node_id, new_equipment_id FROM equipment_migration_log + WHERE migration_status = 'SUCCESS' + LOOP + DELETE FROM mdm_equipment WHERE id = log_record.new_equipment_id; + END LOOP; + + DELETE FROM equipment_migration_log; + + RAISE NOTICE 'Rollback completed'; +END; +$$ LANGUAGE plpgsql; + +-- 7. 后续清理步骤(在新旧系统切换完成后执行) +-- 7.1 标记旧的设备数据为已删除 +-- UPDATE mdm_space_node SET is_deleted = TRUE WHERE is_equipment = TRUE AND id IN ( +-- SELECT old_space_node_id FROM equipment_migration_log WHERE migration_status = 'SUCCESS' +-- ); + +-- 7.2 删除迁移日志表 +-- DROP TABLE IF EXISTS equipment_migration_log; + +COMMENT ON FUNCTION migrate_equipment_from_space_node() IS +'将 mdm_space_node 表中 is_equipment=TRUE 的记录迁移到 mdm_equipment 表'; + +COMMENT ON FUNCTION rollback_equipment_migration() IS +'回滚设备迁移,删除 mdm_equipment 中的迁移记录'; diff --git a/module-mdm/src/main/resources/db/migration/V20260327__add_maintenance_and_inspection_tables.sql b/module-mdm/src/main/resources/db/migration/V20260327__add_maintenance_and_inspection_tables.sql new file mode 100644 index 0000000..4961e12 --- /dev/null +++ b/module-mdm/src/main/resources/db/migration/V20260327__add_maintenance_and_inspection_tables.sql @@ -0,0 +1,122 @@ +-- ============================================ +-- 设备系统分类和维保管理模块 +-- 执行时间: 2026-03-25 +-- 说明: 添加system_type字段,创建维保计划和维保工单表,创建巡检标准项和巡检记录表 +-- ============================================ + +-- 1. 在设备表添加 system_type 字段 +ALTER TABLE mdm_equipment ADD COLUMN system_type VARCHAR(50); +COMMENT ON COLUMN mdm_equipment.system_type IS '商业地产8大系统分类: HVAC-暖通空调, FIRE-消防系统, ELEVATOR-电梯系统, ELECTRICAL-电气系统, PLUMBING-给排水, BAS-弱电智能化, KITCHEN-餐饮厨房, LANDSCAPE-景观, OTHER-其他'; + +-- 2. 创建维保计划表 +CREATE TABLE mdm_maintenance_plan ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + equipment_id UUID NOT NULL, + plan_name VARCHAR(200), + plan_type VARCHAR(50), -- PREVENTIVE预防性/CORRECTIVE纠正性 + cycle_days INTEGER, -- 维保周期天 + last_date DATE, + next_date DATE, + estimated_hours DECIMAL(6,2), + assigned_vendor VARCHAR(200), + status VARCHAR(20) DEFAULT 'ACTIVE', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_mdm_plan_equipment ON mdm_maintenance_plan(equipment_id); +CREATE INDEX idx_mdm_plan_status ON mdm_maintenance_plan(status); +CREATE INDEX idx_mdm_plan_next_date ON mdm_maintenance_plan(next_date); + +-- 3. 创建维保工单表 +CREATE TABLE mdm_maintenance_task ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + task_no VARCHAR(50) UNIQUE, + plan_id UUID, + equipment_id UUID NOT NULL, + task_type VARCHAR(50), -- PREVENTIVE/CORRECTIVE/EMERGENCY + priority VARCHAR(20), -- LOW/MEDIUM/HIGH/URGENT + status VARCHAR(20) DEFAULT 'PENDING', -- PENDING/ASSIGNED/IN_PROGRESS/COMPLETED/CANCELLED + title VARCHAR(200), + description TEXT, + assigned_to VARCHAR(200), + assigned_date DATE, + actual_start TIMESTAMP, + actual_end TIMESTAMP, + actual_hours DECIMAL(6,2), + result TEXT, + parts_used JSONB, + cost DECIMAL(12,2), + completed_by VARCHAR(200), + completed_date DATE, + rating INTEGER, + remark TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_mdm_task_equipment ON mdm_maintenance_task(equipment_id); +CREATE INDEX idx_mdm_task_plan ON mdm_maintenance_task(plan_id); +CREATE INDEX idx_mdm_task_status ON mdm_maintenance_task(status); +CREATE INDEX idx_mdm_task_priority ON mdm_maintenance_task(priority); +CREATE INDEX idx_mdm_task_assigned_date ON mdm_maintenance_task(assigned_date); + +-- 4. 创建巡检标准项表 +CREATE TABLE mdm_inspection_item ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + equipment_type VARCHAR(50), + system_type VARCHAR(50), + item_name VARCHAR(200), + check_method VARCHAR(200), + standard_value VARCHAR(100), + is_required BOOLEAN DEFAULT true, + remark VARCHAR(500), + sort_order INTEGER, + status VARCHAR(20) DEFAULT 'ACTIVE', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_mdm_item_equipment_type ON mdm_inspection_item(equipment_type); +CREATE INDEX idx_mdm_item_system_type ON mdm_inspection_item(system_type); +CREATE INDEX idx_mdm_item_status ON mdm_inspection_item(status); + +-- 5. 创建巡检记录表 +CREATE TABLE mdm_inspection_record ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + plan_id UUID, + equipment_id UUID NOT NULL, + inspection_date DATE NOT NULL, + inspector VARCHAR(200) NOT NULL, + status VARCHAR(20), -- NORMAL/WARNING/ABNORMAL + check_in_time TIMESTAMP, + check_in_location VARCHAR(100), + check_in_photo VARCHAR(200), + items JSONB, -- [{item_id, item_name, value, result, remark}] + problems JSONB, -- [{desc, photo, severity}] + completed BOOLEAN DEFAULT false, + completed_time TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_mdm_record_equipment ON mdm_inspection_record(equipment_id); +CREATE INDEX idx_mdm_record_plan ON mdm_inspection_record(plan_id); +CREATE INDEX idx_mdm_record_inspection_date ON mdm_inspection_record(inspection_date); +CREATE INDEX idx_mdm_record_inspector ON mdm_inspection_record(inspector); +CREATE INDEX idx_mdm_record_status ON mdm_inspection_record(status); + +-- ============================================ +-- 注释 +-- ============================================ + +COMMENT ON TABLE mdm_maintenance_plan IS '维保计划表'; +COMMENT ON TABLE mdm_maintenance_task IS '维保工单表'; +COMMENT ON TABLE mdm_inspection_item IS '巡检标准项表'; +COMMENT ON TABLE mdm_inspection_record IS '巡检记录表'; + +COMMENT ON COLUMN mdm_maintenance_plan.plan_type IS '计划类型: PREVENTIVE-预防性, CORRECTIVE-纠正性'; +COMMENT ON COLUMN mdm_maintenance_plan.status IS '状态: ACTIVE-活跃, INACTIVE-未激活'; +COMMENT ON COLUMN mdm_maintenance_task.task_type IS '任务类型: PREVENTIVE-预防性, CORRECTIVE-纠正性, EMERGENCY-紧急'; +COMMENT ON COLUMN mdm_maintenance_task.priority IS '优先级: LOW-低, MEDIUM-中, HIGH-高, URGENT-紧急'; +COMMENT ON COLUMN mdm_maintenance_task.status IS '状态: PENDING-待分配, ASSIGNED-已分配, IN_PROGRESS-进行中, COMPLETED-已完成, CANCELLED-已取消'; +COMMENT ON COLUMN mdm_inspection_record.status IS '巡检状态: NORMAL-正常, WARNING-警告, ABNORMAL-异常'; \ No newline at end of file diff --git a/module-mdm/src/main/resources/db/migration/V20260328__add_equipment_purchase_fields.sql b/module-mdm/src/main/resources/db/migration/V20260328__add_equipment_purchase_fields.sql new file mode 100644 index 0000000..3bdee28 --- /dev/null +++ b/module-mdm/src/main/resources/db/migration/V20260328__add_equipment_purchase_fields.sql @@ -0,0 +1,15 @@ +-- ============================================ +-- 设备财务字段 +-- 执行时间: 2026-03-28 +-- 说明: 添加设备采购相关财务字段 +-- ============================================ + +-- 添加设备财务字段 +ALTER TABLE mdm_equipment ADD COLUMN purchase_date DATE; +COMMENT ON COLUMN mdm_equipment.purchase_date IS '购置日期'; + +ALTER TABLE mdm_equipment ADD COLUMN purchase_price DECIMAL(12,2); +COMMENT ON COLUMN mdm_equipment.purchase_price IS '购置价格(元)'; + +ALTER TABLE mdm_equipment ADD COLUMN warranty_expire_date DATE; +COMMENT ON COLUMN mdm_equipment.warranty_expire_date IS '保修到期日期'; diff --git a/module-mdm/src/main/resources/db/migration/V20260328__update_maintenance_task_fields.sql b/module-mdm/src/main/resources/db/migration/V20260328__update_maintenance_task_fields.sql new file mode 100644 index 0000000..dc47a1e --- /dev/null +++ b/module-mdm/src/main/resources/db/migration/V20260328__update_maintenance_task_fields.sql @@ -0,0 +1,50 @@ +-- ============================================ +-- 更新维保工单表字段 +-- 执行时间: 2026-03-28 +-- 说明: 添加缺失的字段以支持完整工单流程 +-- ============================================ + +-- 添加缺失字段到维保工单表 +ALTER TABLE mdm_maintenance_task + ADD COLUMN IF NOT EXISTS project_id UUID, + ADD COLUMN IF NOT EXISTS trigger_type VARCHAR(50), -- PLAN/INSPECTION/FAULT/MANUAL + ADD COLUMN IF NOT EXISTS assigned_vendor VARCHAR(200), + ADD COLUMN IF NOT EXISTS fault_cause TEXT, + ADD COLUMN IF NOT EXISTS solution TEXT, + ADD COLUMN IF NOT EXISTS labor_cost DECIMAL(12,2), + ADD COLUMN IF NOT EXISTS parts_cost DECIMAL(12,2), + ADD COLUMN IF NOT EXISTS total_cost DECIMAL(12,2), + ADD COLUMN IF NOT EXISTS verified_by VARCHAR(200), + ADD COLUMN IF NOT EXISTS verified_date DATE, + ADD COLUMN IF NOT EXISTS photos JSONB, + ADD COLUMN IF NOT EXISTS signature TEXT, + ADD COLUMN IF NOT EXISTS created_by VARCHAR(200); + +-- 修改cost字段(如果存在则删除,使用新的费用字段) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_name = 'mdm_maintenance_task' AND column_name = 'cost') THEN + ALTER TABLE mdm_maintenance_task DROP COLUMN cost; + END IF; +END $$; + +-- 添加索引 +CREATE INDEX IF NOT EXISTS idx_mdm_task_project ON mdm_maintenance_task(project_id); +CREATE INDEX IF NOT EXISTS idx_mdm_task_trigger_type ON mdm_maintenance_task(trigger_type); +CREATE INDEX IF NOT EXISTS idx_mdm_task_created_by ON mdm_maintenance_task(created_by); + +-- 更新注释 +COMMENT ON COLUMN mdm_maintenance_task.project_id IS '项目ID'; +COMMENT ON COLUMN mdm_maintenance_task.trigger_type IS '触发类型: PLAN-计划触发, INSPECTION-巡检触发, FAULT-故障触发, MANUAL-手动创建'; +COMMENT ON COLUMN mdm_maintenance_task.assigned_vendor IS '派给维保商'; +COMMENT ON COLUMN mdm_maintenance_task.fault_cause IS '故障原因'; +COMMENT ON COLUMN mdm_maintenance_task.solution IS '解决方案'; +COMMENT ON COLUMN mdm_maintenance_task.labor_cost IS '人工费'; +COMMENT ON COLUMN mdm_maintenance_task.parts_cost IS '备件费'; +COMMENT ON COLUMN mdm_maintenance_task.total_cost IS '总费用'; +COMMENT ON COLUMN mdm_maintenance_task.verified_by IS '验收人'; +COMMENT ON COLUMN mdm_maintenance_task.verified_date IS '验收日期'; +COMMENT ON COLUMN mdm_maintenance_task.photos IS '处理照片URL列表'; +COMMENT ON COLUMN mdm_maintenance_task.signature IS '签名(base64)'; +COMMENT ON COLUMN mdm_maintenance_task.created_by IS '创建人'; diff --git a/module-mdm/src/main/resources/db/migration/V20260329__add_equipment_photos_and_documents.sql b/module-mdm/src/main/resources/db/migration/V20260329__add_equipment_photos_and_documents.sql new file mode 100644 index 0000000..a4a64a2 --- /dev/null +++ b/module-mdm/src/main/resources/db/migration/V20260329__add_equipment_photos_and_documents.sql @@ -0,0 +1,11 @@ +-- 设备照片 +ALTER TABLE mdm_equipment ADD COLUMN photos JSONB; +COMMENT ON COLUMN mdm_equipment.photos IS '设备照片 [{"type":"外观","url":"..."},{"type":"铭牌","url":"..."}]'; + +-- 电子文档 +ALTER TABLE mdm_equipment ADD COLUMN documents JSONB; +COMMENT ON COLUMN mdm_equipment.documents IS '电子文档 [{"name":"说明书.pdf","url":"...","size":1024,"type":"manual"}]'; + +-- 说明书URL(快捷字段) +ALTER TABLE mdm_equipment ADD COLUMN manual_url VARCHAR(500); +COMMENT ON COLUMN mdm_equipment.manual_url IS '设备说明书URL'; \ No newline at end of file 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 deleted file mode 100644 index 5195b63..0000000 --- a/module-mdm/src/test/java/com/ether/pms/mdm/entity/SpaceNodeTest.java +++ /dev/null @@ -1,157 +0,0 @@ -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/EnergyMeterServiceTest.java b/module-mdm/src/test/java/com/ether/pms/mdm/service/EnergyMeterServiceTest.java new file mode 100644 index 0000000..c719cce --- /dev/null +++ b/module-mdm/src/test/java/com/ether/pms/mdm/service/EnergyMeterServiceTest.java @@ -0,0 +1,244 @@ +package com.ether.pms.mdm.service; + +import com.ether.pms.mdm.entity.EnergyMeter; +import com.ether.pms.mdm.repository.EnergyMeterRepository; +import com.ether.pms.mdm.service.impl.EnergyMeterServiceImpl; +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.math.BigDecimal; +import java.util.List; +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.*; + +@ExtendWith(MockitoExtension.class) +class EnergyMeterServiceTest { + + @Mock + private EnergyMeterRepository energyMeterRepository; + + @InjectMocks + private EnergyMeterServiceImpl energyMeterService; + + @Test + void createMeter_shouldCreateMeter_withValidInput() { + UUID projectId = UUID.randomUUID(); + EnergyMeter meter = new EnergyMeter(); + meter.setProjectId(projectId); + meter.setMeterName("照明电表-1"); + meter.setEnergyType(EnergyMeter.EnergyType.LIGHTING); + meter.setInstallationLocation("一楼配电房"); + meter.setRatedCapacity(new BigDecimal("50.00")); + meter.setUnitPrice(new BigDecimal("0.8000")); + + when(energyMeterRepository.existsByMeterCode(any())).thenReturn(false); + when(energyMeterRepository.save(any(EnergyMeter.class))).thenAnswer(invocation -> { + EnergyMeter m = invocation.getArgument(0); + m.setId(UUID.randomUUID()); + return m; + }); + + EnergyMeter result = energyMeterService.createMeter(meter); + + assertNotNull(result); + assertNotNull(result.getId()); + assertEquals("照明电表-1", result.getMeterName()); + assertEquals(EnergyMeter.EnergyType.LIGHTING, result.getEnergyType()); + assertEquals(EnergyMeter.Status.ACTIVE, result.getStatus()); + assertTrue(result.getMeterCode().startsWith("EM")); + } + + @Test + void createMeter_shouldSetDefaultStatus() { + UUID projectId = UUID.randomUUID(); + EnergyMeter meter = new EnergyMeter(); + meter.setProjectId(projectId); + meter.setMeterName("空调电表-1"); + meter.setEnergyType(EnergyMeter.EnergyType.HVAC); + + when(energyMeterRepository.existsByMeterCode(any())).thenReturn(false); + when(energyMeterRepository.save(any(EnergyMeter.class))).thenAnswer(invocation -> { + EnergyMeter m = invocation.getArgument(0); + m.setId(UUID.randomUUID()); + return m; + }); + + EnergyMeter result = energyMeterService.createMeter(meter); + + assertEquals(EnergyMeter.Status.ACTIVE, result.getStatus()); + } + + @Test + void createMeter_shouldSetAllEnergyTypes() { + UUID projectId = UUID.randomUUID(); + + when(energyMeterRepository.existsByMeterCode(any())).thenReturn(false); + when(energyMeterRepository.save(any(EnergyMeter.class))).thenAnswer(invocation -> { + EnergyMeter m = invocation.getArgument(0); + m.setId(UUID.randomUUID()); + return m; + }); + + EnergyMeter lightingMeter = new EnergyMeter(); + lightingMeter.setProjectId(projectId); + lightingMeter.setMeterName("照明"); + lightingMeter.setEnergyType(EnergyMeter.EnergyType.LIGHTING); + + EnergyMeter hvacMeter = new EnergyMeter(); + hvacMeter.setProjectId(projectId); + hvacMeter.setMeterName("空调"); + hvacMeter.setEnergyType(EnergyMeter.EnergyType.HVAC); + + EnergyMeter powerMeter = new EnergyMeter(); + powerMeter.setProjectId(projectId); + powerMeter.setMeterName("动力"); + powerMeter.setEnergyType(EnergyMeter.EnergyType.POWER); + + EnergyMeter specialMeter = new EnergyMeter(); + specialMeter.setProjectId(projectId); + specialMeter.setMeterName("特殊"); + specialMeter.setEnergyType(EnergyMeter.EnergyType.SPECIAL); + + EnergyMeter waterMeter = new EnergyMeter(); + waterMeter.setProjectId(projectId); + waterMeter.setMeterName("给排水"); + waterMeter.setEnergyType(EnergyMeter.EnergyType.WATER); + + EnergyMeter gasMeter = new EnergyMeter(); + gasMeter.setProjectId(projectId); + gasMeter.setMeterName("燃气"); + gasMeter.setEnergyType(EnergyMeter.EnergyType.GAS); + + assertEquals(EnergyMeter.EnergyType.LIGHTING, energyMeterService.createMeter(lightingMeter).getEnergyType()); + assertEquals(EnergyMeter.EnergyType.HVAC, energyMeterService.createMeter(hvacMeter).getEnergyType()); + assertEquals(EnergyMeter.EnergyType.POWER, energyMeterService.createMeter(powerMeter).getEnergyType()); + assertEquals(EnergyMeter.EnergyType.SPECIAL, energyMeterService.createMeter(specialMeter).getEnergyType()); + assertEquals(EnergyMeter.EnergyType.WATER, energyMeterService.createMeter(waterMeter).getEnergyType()); + assertEquals(EnergyMeter.EnergyType.GAS, energyMeterService.createMeter(gasMeter).getEnergyType()); + } + + @Test + void updateMeter_shouldUpdateExistingMeter() { + UUID meterId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + EnergyMeter existingMeter = new EnergyMeter(); + existingMeter.setId(meterId); + existingMeter.setProjectId(projectId); + existingMeter.setMeterCode("EM-TEST-001"); + existingMeter.setMeterName("原名称"); + existingMeter.setEnergyType(EnergyMeter.EnergyType.LIGHTING); + existingMeter.setStatus(EnergyMeter.Status.ACTIVE); + + EnergyMeter updatedMeter = new EnergyMeter(); + updatedMeter.setMeterName("更新后的名称"); + updatedMeter.setEnergyType(EnergyMeter.EnergyType.HVAC); + + when(energyMeterRepository.findById(meterId)).thenReturn(Optional.of(existingMeter)); + when(energyMeterRepository.save(any(EnergyMeter.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + EnergyMeter result = energyMeterService.updateMeter(meterId, updatedMeter); + + assertEquals("更新后的名称", result.getMeterName()); + assertEquals(EnergyMeter.EnergyType.HVAC, result.getEnergyType()); + } + + @Test + void deleteMeter_shouldSoftDelete() { + UUID meterId = UUID.randomUUID(); + EnergyMeter meter = new EnergyMeter(); + meter.setId(meterId); + meter.setStatus(EnergyMeter.Status.ACTIVE); + + when(energyMeterRepository.findById(meterId)).thenReturn(Optional.of(meter)); + when(energyMeterRepository.save(any(EnergyMeter.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + energyMeterService.deleteMeter(meterId); + + assertEquals(EnergyMeter.Status.INACTIVE, meter.getStatus()); + verify(energyMeterRepository).save(meter); + } + + @Test + void getMeterById_shouldReturnMeter_whenExists() { + UUID meterId = UUID.randomUUID(); + EnergyMeter meter = new EnergyMeter(); + meter.setId(meterId); + meter.setMeterCode("EM-2024-001"); + meter.setMeterName("测试仪表"); + + when(energyMeterRepository.findById(meterId)).thenReturn(Optional.of(meter)); + + EnergyMeter result = energyMeterService.getMeterById(meterId); + + assertNotNull(result); + assertEquals(meterId, result.getId()); + } + + @Test + void getMetersByProject_shouldReturnActiveMeters() { + UUID projectId = UUID.randomUUID(); + EnergyMeter meter1 = new EnergyMeter(); + meter1.setId(UUID.randomUUID()); + meter1.setProjectId(projectId); + meter1.setStatus(EnergyMeter.Status.ACTIVE); + + EnergyMeter meter2 = new EnergyMeter(); + meter2.setId(UUID.randomUUID()); + meter2.setProjectId(projectId); + meter2.setStatus(EnergyMeter.Status.ACTIVE); + + when(energyMeterRepository.findByProjectIdAndStatus(projectId, EnergyMeter.Status.ACTIVE)) + .thenReturn(List.of(meter1, meter2)); + + List results = energyMeterService.getMetersByProject(projectId); + + assertEquals(2, results.size()); + } + + @Test + void getMetersByType_shouldReturnMatchingMeters() { + UUID projectId = UUID.randomUUID(); + EnergyMeter hvacMeter = new EnergyMeter(); + hvacMeter.setId(UUID.randomUUID()); + hvacMeter.setProjectId(projectId); + hvacMeter.setEnergyType(EnergyMeter.EnergyType.HVAC); + + when(energyMeterRepository.findByProjectIdAndEnergyType(projectId, EnergyMeter.EnergyType.HVAC)) + .thenReturn(List.of(hvacMeter)); + + List results = energyMeterService.getMetersByType(projectId, EnergyMeter.EnergyType.HVAC); + + assertEquals(1, results.size()); + assertEquals(EnergyMeter.EnergyType.HVAC, results.get(0).getEnergyType()); + } + + @Test + void createMeter_shouldSetRatedCapacityAndUnitPrice() { + UUID projectId = UUID.randomUUID(); + EnergyMeter meter = new EnergyMeter(); + meter.setProjectId(projectId); + meter.setMeterName("动力电表"); + meter.setEnergyType(EnergyMeter.EnergyType.POWER); + meter.setRatedCapacity(new BigDecimal("100.00")); + meter.setUnitPrice(new BigDecimal("0.8500")); + + when(energyMeterRepository.existsByMeterCode(any())).thenReturn(false); + when(energyMeterRepository.save(any(EnergyMeter.class))).thenAnswer(invocation -> { + EnergyMeter m = invocation.getArgument(0); + m.setId(UUID.randomUUID()); + return m; + }); + + EnergyMeter result = energyMeterService.createMeter(meter); + + assertEquals(new BigDecimal("100.00"), result.getRatedCapacity()); + assertEquals(new BigDecimal("0.8500"), result.getUnitPrice()); + } +} \ No newline at end of file diff --git a/module-mdm/src/test/java/com/ether/pms/mdm/service/InspectionTemplateServiceTest.java b/module-mdm/src/test/java/com/ether/pms/mdm/service/InspectionTemplateServiceTest.java new file mode 100644 index 0000000..ece623a --- /dev/null +++ b/module-mdm/src/test/java/com/ether/pms/mdm/service/InspectionTemplateServiceTest.java @@ -0,0 +1,206 @@ +package com.ether.pms.mdm.service; + +import com.ether.pms.mdm.entity.InspectionTemplate; +import com.ether.pms.mdm.repository.InspectionTemplateRepository; +import com.ether.pms.mdm.service.impl.InspectionTemplateServiceImpl; +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.util.List; +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.*; + +@ExtendWith(MockitoExtension.class) +class InspectionTemplateServiceTest { + + @Mock + private InspectionTemplateRepository inspectionTemplateRepository; + + @InjectMocks + private InspectionTemplateServiceImpl inspectionTemplateService; + + @Test + void createTemplate_shouldCreateTemplate_withValidInput() { + UUID projectId = UUID.randomUUID(); + InspectionTemplate template = new InspectionTemplate(); + template.setProjectId(projectId); + template.setTemplateName("电梯月度点检模板"); + template.setEquipmentType("电梯"); + template.setInspectionItems("1. 检查轿厢整洁\n2. 检查门机系统\n3. 测试急停开关"); + template.setEstimatedDuration(30); + + when(inspectionTemplateRepository.existsByTemplateCode(any())).thenReturn(false); + when(inspectionTemplateRepository.save(any(InspectionTemplate.class))).thenAnswer(invocation -> { + InspectionTemplate t = invocation.getArgument(0); + t.setId(UUID.randomUUID()); + return t; + }); + + InspectionTemplate result = inspectionTemplateService.createTemplate(template); + + assertNotNull(result); + assertNotNull(result.getId()); + assertEquals("电梯月度点检模板", result.getTemplateName()); + assertEquals("电梯", result.getEquipmentType()); + assertEquals(InspectionTemplate.Status.ACTIVE, result.getStatus()); + assertTrue(result.getTemplateCode().startsWith("IT")); + } + + @Test + void createTemplate_shouldSetDefaultStatus() { + UUID projectId = UUID.randomUUID(); + InspectionTemplate template = new InspectionTemplate(); + template.setProjectId(projectId); + template.setTemplateName("空调季度点检"); + template.setEquipmentType("中央空调"); + + when(inspectionTemplateRepository.existsByTemplateCode(any())).thenReturn(false); + when(inspectionTemplateRepository.save(any(InspectionTemplate.class))).thenAnswer(invocation -> { + InspectionTemplate t = invocation.getArgument(0); + t.setId(UUID.randomUUID()); + return t; + }); + + InspectionTemplate result = inspectionTemplateService.createTemplate(template); + + assertEquals(InspectionTemplate.Status.ACTIVE, result.getStatus()); + } + + @Test + void updateTemplate_shouldUpdateExistingTemplate() { + UUID templateId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + InspectionTemplate existingTemplate = new InspectionTemplate(); + existingTemplate.setId(templateId); + existingTemplate.setProjectId(projectId); + existingTemplate.setTemplateCode("IT-TEST-001"); + existingTemplate.setTemplateName("原模板名称"); + existingTemplate.setEquipmentType("电梯"); + existingTemplate.setStatus(InspectionTemplate.Status.ACTIVE); + + InspectionTemplate updatedTemplate = new InspectionTemplate(); + updatedTemplate.setTemplateName("更新后的模板名称"); + updatedTemplate.setEstimatedDuration(60); + + when(inspectionTemplateRepository.findById(templateId)).thenReturn(Optional.of(existingTemplate)); + when(inspectionTemplateRepository.save(any(InspectionTemplate.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + InspectionTemplate result = inspectionTemplateService.updateTemplate(templateId, updatedTemplate); + + assertEquals("更新后的模板名称", result.getTemplateName()); + assertEquals(60, result.getEstimatedDuration()); + } + + @Test + void copyTemplate_shouldCreateCopyWithIncrementedVersion() { + UUID originalId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + InspectionTemplate originalTemplate = new InspectionTemplate(); + originalTemplate.setId(originalId); + originalTemplate.setProjectId(projectId); + originalTemplate.setTemplateCode("IT-ORIG-001"); + originalTemplate.setTemplateName("原始模板"); + originalTemplate.setEquipmentType("电梯"); + originalTemplate.setInspectionItems("检查项目列表"); + originalTemplate.setEstimatedDuration(30); + originalTemplate.setVersion(1); + originalTemplate.setCreatedBy("admin"); + + when(inspectionTemplateRepository.findById(originalId)).thenReturn(Optional.of(originalTemplate)); + when(inspectionTemplateRepository.existsByTemplateCode(any())).thenReturn(false); + when(inspectionTemplateRepository.save(any(InspectionTemplate.class))).thenAnswer(invocation -> { + InspectionTemplate t = invocation.getArgument(0); + t.setId(UUID.randomUUID()); + return t; + }); + + InspectionTemplate result = inspectionTemplateService.copyTemplate(originalId, "复制模板"); + + assertNotNull(result); + assertEquals("复制模板", result.getTemplateName()); + assertEquals("电梯", result.getEquipmentType()); + assertEquals(2, result.getVersion()); + assertEquals(InspectionTemplate.Status.ACTIVE, result.getStatus()); + } + + @Test + void getTemplatesByType_shouldReturnMatchingTemplates() { + UUID projectId = UUID.randomUUID(); + InspectionTemplate elevatorTemplate = new InspectionTemplate(); + elevatorTemplate.setId(UUID.randomUUID()); + elevatorTemplate.setProjectId(projectId); + elevatorTemplate.setEquipmentType("电梯"); + + when(inspectionTemplateRepository.findByEquipmentType("电梯")) + .thenReturn(List.of(elevatorTemplate)); + + List results = inspectionTemplateService.getTemplatesByType("电梯"); + + assertEquals(1, results.size()); + assertEquals("电梯", results.get(0).getEquipmentType()); + } + + @Test + void getTemplatesByProject_shouldReturnProjectTemplates() { + UUID projectId = UUID.randomUUID(); + InspectionTemplate template1 = new InspectionTemplate(); + template1.setId(UUID.randomUUID()); + template1.setProjectId(projectId); + + InspectionTemplate template2 = new InspectionTemplate(); + template2.setId(UUID.randomUUID()); + template2.setProjectId(projectId); + + when(inspectionTemplateRepository.findByProjectId(projectId)) + .thenReturn(List.of(template1, template2)); + + List results = inspectionTemplateService.getTemplatesByProject(projectId); + + assertEquals(2, results.size()); + } + + @Test + void getTemplateById_shouldReturnTemplate_whenExists() { + UUID templateId = UUID.randomUUID(); + InspectionTemplate template = new InspectionTemplate(); + template.setId(templateId); + template.setTemplateCode("IT-2024-001"); + template.setTemplateName("测试模板"); + + when(inspectionTemplateRepository.findById(templateId)).thenReturn(Optional.of(template)); + + InspectionTemplate result = inspectionTemplateService.getTemplateById(templateId); + + assertNotNull(result); + assertEquals(templateId, result.getId()); + } + + @Test + void createTemplate_shouldSetInspectionItems() { + UUID projectId = UUID.randomUUID(); + InspectionTemplate template = new InspectionTemplate(); + template.setProjectId(projectId); + template.setTemplateName("消防设备点检"); + template.setEquipmentType("消防"); + template.setInspectionItems("1. 检查灭火器压力\n2. 检查消防泵\n3. 测试报警系统"); + + when(inspectionTemplateRepository.existsByTemplateCode(any())).thenReturn(false); + when(inspectionTemplateRepository.save(any(InspectionTemplate.class))).thenAnswer(invocation -> { + InspectionTemplate t = invocation.getArgument(0); + t.setId(UUID.randomUUID()); + return t; + }); + + InspectionTemplate result = inspectionTemplateService.createTemplate(template); + + assertNotNull(result.getInspectionItems()); + assertTrue(result.getInspectionItems().contains("灭火器")); + } +} \ No newline at end of file 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 deleted file mode 100644 index 28a3d47..0000000 --- a/module-mdm/src/test/java/com/ether/pms/mdm/service/ProjectStatisticsServiceTest.java +++ /dev/null @@ -1,140 +0,0 @@ -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/module-mdm/src/test/java/com/ether/pms/mdm/service/SparePartServiceTest.java b/module-mdm/src/test/java/com/ether/pms/mdm/service/SparePartServiceTest.java new file mode 100644 index 0000000..535013c --- /dev/null +++ b/module-mdm/src/test/java/com/ether/pms/mdm/service/SparePartServiceTest.java @@ -0,0 +1,265 @@ +package com.ether.pms.mdm.service; + +import com.ether.pms.mdm.entity.SparePart; +import com.ether.pms.mdm.entity.SparePartCategory; +import com.ether.pms.mdm.entity.SparePartRecord; +import com.ether.pms.mdm.repository.SparePartCategoryRepository; +import com.ether.pms.mdm.repository.SparePartRecordRepository; +import com.ether.pms.mdm.repository.SparePartRepository; +import com.ether.pms.mdm.service.impl.SparePartServiceImpl; +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.math.BigDecimal; +import java.util.List; +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.*; + +@ExtendWith(MockitoExtension.class) +class SparePartServiceTest { + + @Mock + private SparePartRepository sparePartRepository; + + @Mock + private SparePartCategoryRepository sparePartCategoryRepository; + + @Mock + private SparePartRecordRepository sparePartRecordRepository; + + @InjectMocks + private SparePartServiceImpl sparePartService; + + @Test + void createSparePart_shouldCreateSparePart_withValidInput() { + UUID projectId = UUID.randomUUID(); + SparePart sparePart = new SparePart(); + sparePart.setProjectId(projectId); + sparePart.setSparePartName("电梯润滑油"); + sparePart.setUnit("桶"); + sparePart.setSpecification("18L/桶"); + sparePart.setSafeStock(10); + sparePart.setUnitPrice(new BigDecimal("299.00")); + + when(sparePartRepository.existsBySparePartCode(any())).thenReturn(false); + when(sparePartRepository.save(any(SparePart.class))).thenAnswer(invocation -> { + SparePart sp = invocation.getArgument(0); + sp.setId(UUID.randomUUID()); + return sp; + }); + + SparePart result = sparePartService.createSparePart(sparePart); + + assertNotNull(result); + assertNotNull(result.getId()); + assertEquals("电梯润滑油", result.getSparePartName()); + assertEquals("桶", result.getUnit()); + assertEquals(SparePart.Status.ACTIVE, result.getStatus()); + assertTrue(result.getSparePartCode().startsWith("SP")); + } + + @Test + void createSparePart_shouldInitializeCurrentStockToZero() { + UUID projectId = UUID.randomUUID(); + SparePart sparePart = new SparePart(); + sparePart.setProjectId(projectId); + sparePart.setSparePartName("螺丝"); + sparePart.setUnit("盒"); + + when(sparePartRepository.existsBySparePartCode(any())).thenReturn(false); + when(sparePartRepository.save(any(SparePart.class))).thenAnswer(invocation -> { + SparePart sp = invocation.getArgument(0); + sp.setId(UUID.randomUUID()); + return sp; + }); + + SparePart result = sparePartService.createSparePart(sparePart); + + assertEquals(0, result.getCurrentStock()); + } + + @Test + void updateSparePart_shouldUpdateExistingSparePart() { + UUID sparePartId = UUID.randomUUID(); + UUID projectId = UUID.randomUUID(); + SparePart existingPart = new SparePart(); + existingPart.setId(sparePartId); + existingPart.setProjectId(projectId); + existingPart.setSparePartCode("SP-TEST-001"); + existingPart.setSparePartName("原名称"); + existingPart.setStatus(SparePart.Status.ACTIVE); + + SparePart updatedPart = new SparePart(); + updatedPart.setSparePartName("更新后的名称"); + updatedPart.setUnitPrice(new BigDecimal("399.00")); + + when(sparePartRepository.findById(sparePartId)).thenReturn(Optional.of(existingPart)); + when(sparePartRepository.save(any(SparePart.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + SparePart result = sparePartService.updateSparePart(sparePartId, updatedPart); + + assertEquals("更新后的名称", result.getSparePartName()); + assertEquals(new BigDecimal("399.00"), result.getUnitPrice()); + } + + @Test + void deleteSparePart_shouldSoftDelete() { + UUID sparePartId = UUID.randomUUID(); + SparePart sparePart = new SparePart(); + sparePart.setId(sparePartId); + sparePart.setStatus(SparePart.Status.ACTIVE); + + when(sparePartRepository.findById(sparePartId)).thenReturn(Optional.of(sparePart)); + when(sparePartRepository.save(any(SparePart.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + sparePartService.deleteSparePart(sparePartId); + + assertEquals(SparePart.Status.INACTIVE, sparePart.getStatus()); + verify(sparePartRepository).save(sparePart); + } + + @Test + void getSparePartById_shouldReturnSparePart_whenExists() { + UUID sparePartId = UUID.randomUUID(); + SparePart sparePart = new SparePart(); + sparePart.setId(sparePartId); + sparePart.setSparePartCode("SP-2024-001"); + sparePart.setSparePartName("测试备件"); + + when(sparePartRepository.findById(sparePartId)).thenReturn(Optional.of(sparePart)); + + SparePart result = sparePartService.getSparePartById(sparePartId); + + assertNotNull(result); + assertEquals(sparePartId, result.getId()); + } + + @Test + void getSparePartsByProject_shouldReturnActiveSpareParts() { + UUID projectId = UUID.randomUUID(); + SparePart part1 = new SparePart(); + part1.setId(UUID.randomUUID()); + part1.setProjectId(projectId); + part1.setStatus(SparePart.Status.ACTIVE); + + SparePart part2 = new SparePart(); + part2.setId(UUID.randomUUID()); + part2.setProjectId(projectId); + part2.setStatus(SparePart.Status.ACTIVE); + + when(sparePartRepository.findByProjectIdAndStatus(projectId, SparePart.Status.ACTIVE)) + .thenReturn(List.of(part1, part2)); + + List results = sparePartService.getSparePartsByProject(projectId); + + assertEquals(2, results.size()); + } + + @Test + void inStock_shouldIncreaseStockAndCreateRecord() { + UUID sparePartId = UUID.randomUUID(); + UUID recordedBy = UUID.randomUUID(); + SparePart sparePart = new SparePart(); + sparePart.setId(sparePartId); + sparePart.setCurrentStock(10); + sparePart.setSparePartCode("SP-001"); + + when(sparePartRepository.findById(sparePartId)).thenReturn(Optional.of(sparePart)); + when(sparePartRepository.save(any(SparePart.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(sparePartRecordRepository.save(any(SparePartRecord.class))).thenAnswer(invocation -> { + SparePartRecord record = invocation.getArgument(0); + record.setId(UUID.randomUUID()); + return record; + }); + + SparePartRecord result = sparePartService.inStock(sparePartId, 5, recordedBy, "新批次入库"); + + assertEquals(15, sparePart.getCurrentStock()); + assertEquals(SparePartRecord.RecordType.IN, result.getRecordType()); + assertEquals(5, result.getQuantity()); + assertEquals(15, result.getBalance()); + } + + @Test + void outStock_shouldDecreaseStockAndCreateRecord() { + UUID sparePartId = UUID.randomUUID(); + UUID recordedBy = UUID.randomUUID(); + UUID orderId = UUID.randomUUID(); + SparePart sparePart = new SparePart(); + sparePart.setId(sparePartId); + sparePart.setCurrentStock(10); + sparePart.setSparePartCode("SP-001"); + + when(sparePartRepository.findById(sparePartId)).thenReturn(Optional.of(sparePart)); + when(sparePartRepository.save(any(SparePart.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(sparePartRecordRepository.save(any(SparePartRecord.class))).thenAnswer(invocation -> { + SparePartRecord record = invocation.getArgument(0); + record.setId(UUID.randomUUID()); + return record; + }); + + SparePartRecord result = sparePartService.outStock(sparePartId, 3, orderId, recordedBy, "维修使用"); + + assertEquals(7, sparePart.getCurrentStock()); + assertEquals(SparePartRecord.RecordType.OUT, result.getRecordType()); + assertEquals(3, result.getQuantity()); + assertEquals(7, result.getBalance()); + } + + @Test + void outStock_shouldThrowException_whenInsufficientStock() { + UUID sparePartId = UUID.randomUUID(); + UUID recordedBy = UUID.randomUUID(); + SparePart sparePart = new SparePart(); + sparePart.setId(sparePartId); + sparePart.setCurrentStock(2); + + when(sparePartRepository.findById(sparePartId)).thenReturn(Optional.of(sparePart)); + + assertThrows(Exception.class, () -> sparePartService.outStock(sparePartId, 5, null, recordedBy, "测试")); + } + + @Test + void getLowStockParts_shouldReturnPartsBelowSafeStock() { + UUID projectId = UUID.randomUUID(); + SparePart lowStockPart = new SparePart(); + lowStockPart.setId(UUID.randomUUID()); + lowStockPart.setProjectId(projectId); + lowStockPart.setCurrentStock(3); + lowStockPart.setSafeStock(10); + + when(sparePartRepository.findLowStockParts(projectId)) + .thenReturn(List.of(lowStockPart)); + + List results = sparePartService.getLowStockParts(projectId); + + assertEquals(1, results.size()); + assertTrue(results.get(0).getCurrentStock() < results.get(0).getSafeStock()); + } + + @Test + void createCategory_shouldCreateCategory() { + SparePartCategory category = new SparePartCategory(); + category.setCategoryName("电气配件"); + + when(sparePartCategoryRepository.findByCategoryCode(any())).thenReturn(Optional.empty()); + when(sparePartCategoryRepository.save(any(SparePartCategory.class))).thenAnswer(invocation -> { + SparePartCategory c = invocation.getArgument(0); + c.setId(UUID.randomUUID()); + return c; + }); + + SparePartCategory result = sparePartService.createCategory(category); + + assertNotNull(result); + assertNotNull(result.getId()); + assertTrue(result.getCategoryCode().startsWith("CC")); + } +} \ No newline at end of file diff --git a/module-ops/pom.xml b/module-wo/pom.xml similarity index 69% rename from module-ops/pom.xml rename to module-wo/pom.xml index c26e42e..83a8906 100644 --- a/module-ops/pom.xml +++ b/module-wo/pom.xml @@ -10,11 +10,11 @@ 1.0.0-SNAPSHOT - module-ops + module-wo jar - Module OPS - 运营调度模块 + Module WO + 工单运营模块 - Work Order @@ -23,6 +23,18 @@ ${project.version} + + com.ether + module-mdm + ${project.version} + + + + com.ether + module-asset + ${project.version} + + org.springframework.boot spring-boot-starter-web @@ -53,5 +65,12 @@ org.mapstruct mapstruct + + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/module-wo/src/main/java/com/ether/pms/ops/controller/MaintenanceTaskController.java b/module-wo/src/main/java/com/ether/pms/ops/controller/MaintenanceTaskController.java new file mode 100644 index 0000000..f70b6d0 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/controller/MaintenanceTaskController.java @@ -0,0 +1,136 @@ +package com.ether.pms.ops.controller; + +import com.ether.pms.common.ApiResponse; +import com.ether.pms.ops.dto.MaintenanceTaskStatsDTO; +import com.ether.pms.ops.entity.MaintenanceTask; +import com.ether.pms.ops.service.MaintenanceTaskService; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * 维保工单控制器 + */ +@RestController +@RequestMapping("/api/ops/maintenance-tasks") +@RequiredArgsConstructor +public class MaintenanceTaskController { + + private final MaintenanceTaskService maintenanceTaskService; + + @PostMapping + public ApiResponse createTask(@RequestBody MaintenanceTask task) { + return ApiResponse.success(maintenanceTaskService.createTask(task)); + } + + @GetMapping + public ApiResponse> getTasks( + @RequestParam(required = false) UUID equipmentId, + @RequestParam(required = false) UUID planId, + @RequestParam(required = false) MaintenanceTask.Status status, + @RequestParam(required = false) MaintenanceTask.Priority priority, + @RequestParam(required = false) String assignedTo, + @RequestParam(required = false) LocalDate overdueDate) { + List tasks; + if (equipmentId != null) { + tasks = maintenanceTaskService.getTasksByEquipment(equipmentId); + } else if (planId != null) { + tasks = maintenanceTaskService.getTasksByPlan(planId); + } else if (status != null) { + tasks = maintenanceTaskService.getTasksByStatus(status); + } else if (priority != null) { + tasks = maintenanceTaskService.getTasksByPriority(priority); + } else if (assignedTo != null) { + tasks = maintenanceTaskService.getTasksByAssignedTo(assignedTo); + } else if (overdueDate != null) { + tasks = maintenanceTaskService.getOverdueTasks(overdueDate); + } else { + tasks = maintenanceTaskService.getAllTasks(); + } + return ApiResponse.success(tasks); + } + + @GetMapping("/{id}") + public ApiResponse getTask(@PathVariable UUID id) { + return ApiResponse.success(maintenanceTaskService.getTaskById(id)); + } + + @PutMapping("/{id}") + public ApiResponse updateTask(@PathVariable UUID id, @RequestBody MaintenanceTask task) { + return ApiResponse.success(maintenanceTaskService.updateTask(id, task)); + } + + @DeleteMapping("/{id}") + public ApiResponse deleteTask(@PathVariable UUID id) { + maintenanceTaskService.deleteTask(id); + return ApiResponse.success(null); + } + + @PostMapping("/{id}/assign") + public ApiResponse assignTask(@PathVariable UUID id, @RequestBody AssignRequest request) { + return ApiResponse.success(maintenanceTaskService.assignTask(id, request.getAssignedTo(), request.getAssignedDate())); + } + + @PostMapping("/{id}/start") + public ApiResponse startTask(@PathVariable UUID id) { + return ApiResponse.success(maintenanceTaskService.startTask(id)); + } + + @PostMapping("/{id}/complete") + public ApiResponse completeTask(@PathVariable UUID id, @RequestBody CompleteRequest request) { + return ApiResponse.success(maintenanceTaskService.completeTask( + id, request.getResult(), request.getActualHours(), request.getCost(), request.getCompletedBy())); + } + + @PostMapping("/{id}/complete-details") + public ApiResponse completeTaskWithDetails(@PathVariable UUID id, @RequestBody MaintenanceTask taskData) { + return ApiResponse.success(maintenanceTaskService.completeTaskWithDetails(id, taskData)); + } + + @PostMapping("/{id}/verify") + public ApiResponse verifyTask(@PathVariable UUID id, @RequestBody VerifyRequest request) { + return ApiResponse.success(maintenanceTaskService.verifyTask( + id, request.getVerifiedBy(), request.getRemark(), request.getRating())); + } + + @PostMapping("/{id}/cancel") + public ApiResponse cancelTask(@PathVariable UUID id) { + return ApiResponse.success(maintenanceTaskService.cancelTask(id)); + } + + @PostMapping("/{id}/rate") + public ApiResponse rateTask(@PathVariable UUID id, @RequestParam Integer rating) { + return ApiResponse.success(maintenanceTaskService.rateTask(id, rating)); + } + + @GetMapping("/stats") + public ApiResponse getTaskStats() { + return ApiResponse.success(maintenanceTaskService.getTaskStats()); + } + + @Data + public static class AssignRequest { + private String assignedTo; + private LocalDate assignedDate; + } + + @Data + public static class CompleteRequest { + private String result; + private BigDecimal actualHours; + private BigDecimal cost; + private String completedBy; + } + + @Data + public static class VerifyRequest { + private String verifiedBy; + private String remark; + private Integer rating; + } +} diff --git a/module-wo/src/main/java/com/ether/pms/ops/controller/WorkOrderController.java b/module-wo/src/main/java/com/ether/pms/ops/controller/WorkOrderController.java new file mode 100644 index 0000000..051b7af --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/controller/WorkOrderController.java @@ -0,0 +1,125 @@ +package com.ether.pms.ops.controller; + +import com.ether.pms.common.ApiResponse; +import com.ether.pms.ops.dto.WorkOrderStatsDTO; +import com.ether.pms.ops.entity.WorkOrder; +import com.ether.pms.ops.entity.WorkOrderItem; +import com.ether.pms.ops.service.WorkOrderService; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/wo/work-orders") +@RequiredArgsConstructor +public class WorkOrderController { + + private final WorkOrderService workOrderService; + + @PostMapping + public ApiResponse create(@RequestBody WorkOrder workOrder) { + return ApiResponse.success(workOrderService.createWorkOrder(workOrder)); + } + + @GetMapping + public ApiResponse> list( + @RequestParam(required = false) UUID projectId, + @RequestParam(required = false) UUID equipmentId, + @RequestParam(required = false) WorkOrder.Source source, + @RequestParam(required = false) WorkOrder.Type type, + @RequestParam(required = false) WorkOrder.Status status, + @RequestParam(required = false) String assignedTo) { + List list; + if (projectId != null) { + list = workOrderService.getWorkOrdersByProject(projectId); + } else if (equipmentId != null) { + list = workOrderService.getWorkOrdersByEquipment(equipmentId); + } else if (source != null) { + list = workOrderService.getWorkOrdersBySource(source); + } else if (type != null) { + list = workOrderService.getWorkOrdersByType(type); + } else if (status != null) { + list = workOrderService.getWorkOrdersByStatus(status); + } else if (assignedTo != null) { + list = workOrderService.getWorkOrdersByAssignedTo(assignedTo); + } else { + list = workOrderService.getAllWorkOrders(); + } + return ApiResponse.success(list); + } + + @GetMapping("/{id}") + public ApiResponse get(@PathVariable UUID id) { + return ApiResponse.success(workOrderService.getWorkOrderById(id)); + } + + @PutMapping("/{id}") + public ApiResponse update(@PathVariable UUID id, @RequestBody WorkOrder workOrder) { + return ApiResponse.success(workOrderService.updateWorkOrder(id, workOrder)); + } + + @DeleteMapping("/{id}") + public ApiResponse delete(@PathVariable UUID id) { + workOrderService.deleteWorkOrder(id); + return ApiResponse.success(null); + } + + @PostMapping("/{id}/assign") + public ApiResponse assign(@PathVariable UUID id, @RequestBody AssignRequest request) { + return ApiResponse.success(workOrderService.assignWorkOrder(id, request.getAssignedTo(), request.getAssignedVendor(), request.getAssignedDate())); + } + + @PostMapping("/{id}/start") + public ApiResponse start(@PathVariable UUID id) { + return ApiResponse.success(workOrderService.startWorkOrder(id)); + } + + @PostMapping("/{id}/complete") + public ApiResponse complete(@PathVariable UUID id, @RequestBody WorkOrder data) { + return ApiResponse.success(workOrderService.completeWorkOrder(id, data)); + } + + @PostMapping("/{id}/verify") + public ApiResponse verify(@PathVariable UUID id, @RequestBody VerifyRequest request) { + return ApiResponse.success(workOrderService.verifyWorkOrder(id, request.getVerifiedBy(), request.getRemark(), request.getRating())); + } + + @PostMapping("/{id}/cancel") + public ApiResponse cancel(@PathVariable UUID id) { + return ApiResponse.success(workOrderService.cancelWorkOrder(id)); + } + + @GetMapping("/stats") + public ApiResponse stats() { + return ApiResponse.success(workOrderService.getWorkOrderStats()); + } + + @GetMapping("/{id}/items") + public ApiResponse> items(@PathVariable UUID id) { + return ApiResponse.success(workOrderService.getWorkOrderItems(id)); + } + + @PostMapping("/{id}/items") + public ApiResponse addItems(@PathVariable UUID id, @RequestBody List items) { + return ApiResponse.success(workOrderService.addWorkOrderItems(id, items)); + } + + @Data + public static class AssignRequest { + private String assignedTo; + private String assignedVendor; + private LocalDate assignedDate; + } + + @Data + public static class VerifyRequest { + private String verifiedBy; + private String remark; + private Integer rating; + } +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/dto/MaintenanceTaskStatsDTO.java b/module-wo/src/main/java/com/ether/pms/ops/dto/MaintenanceTaskStatsDTO.java new file mode 100644 index 0000000..558d2dd --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/dto/MaintenanceTaskStatsDTO.java @@ -0,0 +1,44 @@ +package com.ether.pms.ops.dto; + +import lombok.Builder; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Map; + +/** + * 维保工单统计DTO + */ +@Data +@Builder +public class MaintenanceTaskStatsDTO { + + // 基础统计 + private long total; + private long pending; + private long assigned; + private long inProgress; + private long completed; + private long verified; + private long cancelled; + + // 今日统计 + private long completedToday; + private long createdToday; + + // 异常统计 + private long overdue; + + // 效率指标 + private BigDecimal avgCompleteHours; + private BigDecimal avgRating; + + // 分布统计 + private Map byPriority; + private Map byTriggerType; + + // 费用统计 + private BigDecimal totalLaborCost; + private BigDecimal totalPartsCost; + private BigDecimal totalCost; +} diff --git a/module-wo/src/main/java/com/ether/pms/ops/dto/WorkOrderStatsDTO.java b/module-wo/src/main/java/com/ether/pms/ops/dto/WorkOrderStatsDTO.java new file mode 100644 index 0000000..450d30a --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/dto/WorkOrderStatsDTO.java @@ -0,0 +1,27 @@ +package com.ether.pms.ops.dto; + +import lombok.Builder; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Map; + +@Data +@Builder +public class WorkOrderStatsDTO { + private long total; + private long pending; + private long assigned; + private long inProgress; + private long completed; + private long verified; + private long cancelled; + private long completedToday; + private long createdToday; + private long overdue; + private BigDecimal avgCompleteHours; + private BigDecimal avgRating; + private Map bySource; + private Map byType; + private Map byPriority; +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/entity/InspectionItem.java b/module-wo/src/main/java/com/ether/pms/ops/entity/InspectionItem.java new file mode 100644 index 0000000..3d03ca9 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/entity/InspectionItem.java @@ -0,0 +1,48 @@ +package com.ether.pms.ops.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity(name = "OpsInspectionItem") +@Table(name = "ops_inspection_item") +@Data +public class InspectionItem { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "template_id", nullable = false) + private UUID templateId; + + @Column(name = "item_name", nullable = false, length = 200) + private String itemName; + + @Column(columnDefinition = "TEXT") + private String description; + + @Column(name = "check_method", columnDefinition = "TEXT") + private String checkMethod; + + @Column(columnDefinition = "TEXT") + private String standard; + + @Column(name = "is_mandatory") + private Boolean isMandatory = true; + + @Column(name = "is_normal_required") + private Boolean isNormalRequired = true; + + @Column(name = "sort_order") + private Integer sortOrder = 0; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @PrePersist + public void prePersist() { + createdAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/entity/InspectionTemplate.java b/module-wo/src/main/java/com/ether/pms/ops/entity/InspectionTemplate.java new file mode 100644 index 0000000..42ff3cf --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/entity/InspectionTemplate.java @@ -0,0 +1,66 @@ +package com.ether.pms.ops.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Entity(name = "OpsInspectionTemplate") +@Table(name = "ops_inspection_template") +@Data +public class InspectionTemplate { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "template_code", unique = true, nullable = false, length = 50) + private String templateCode; + + @Column(name = "template_name", nullable = false, length = 200) + private String templateName; + + @Column(columnDefinition = "TEXT") + private String description; + + @Column(name = "project_id") + private UUID projectId; + + @Column(name = "space_id") + private UUID spaceId; + + @Column(length = 50) + private String category; + + @Column(length = 20) + @Enumerated(EnumType.STRING) + private TemplateStatus status = TemplateStatus.ACTIVE; + + public enum TemplateStatus { + ACTIVE, INACTIVE + } + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "created_by", length = 200) + private String createdBy; + + @Transient + private List items; + + @PrePersist + public void prePersist() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/entity/MaintenancePlan.java b/module-wo/src/main/java/com/ether/pms/ops/entity/MaintenancePlan.java new file mode 100644 index 0000000..9671df6 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/entity/MaintenancePlan.java @@ -0,0 +1,83 @@ +package com.ether.pms.ops.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "ops_maintenance_plan") +@Data +public class MaintenancePlan { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "plan_code", unique = true, nullable = false, length = 50) + private String planCode; + + @Column(name = "plan_name", nullable = false, length = 200) + private String planName; + + @Column(name = "plan_content", columnDefinition = "TEXT") + private String planContent; + + @Column(name = "project_id") + private UUID projectId; + + @Column(name = "equipment_id", nullable = false) + private UUID equipmentId; + + @Column(name = "plan_type", nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private PlanType planType; + + public enum PlanType { + PREVENTIVE, CORRECTIVE + } + + @Column(name = "cycle_days") + private Integer cycleDays; + + @Column(name = "estimated_hours", precision = 6, scale = 2) + private java.math.BigDecimal estimatedHours; + + @Column(name = "assigned_vendor", length = 200) + private String assignedVendor; + + @Column(length = 20) + @Enumerated(EnumType.STRING) + private PlanStatus status = PlanStatus.ACTIVE; + + public enum PlanStatus { + ACTIVE, INACTIVE, SUSPENDED + } + + @Column(name = "last_date") + private LocalDate lastDate; + + @Column(name = "next_date") + private LocalDate nextDate; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "created_by", length = 200) + private String createdBy; + + @PrePersist + public void prePersist() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/entity/MaintenanceTask.java b/module-wo/src/main/java/com/ether/pms/ops/entity/MaintenanceTask.java new file mode 100644 index 0000000..bfa5bb7 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/entity/MaintenanceTask.java @@ -0,0 +1,171 @@ +package com.ether.pms.ops.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * 维保工单实体(商业地产维保管理) + */ +@Entity +@Table(name = "ops_maintenance_task") +@Data +public class MaintenanceTask { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "task_no", unique = true, length = 50) + private String taskNo; + + @Column(name = "plan_id") + private UUID planId; + + @Column(name = "equipment_id", nullable = false) + private UUID equipmentId; + + @Column(name = "project_id") + private UUID projectId; + + @Column(name = "task_type", length = 50) + @Enumerated(EnumType.STRING) + private TaskType taskType; + + public enum TaskType { + PREVENTIVE, // 预防性 + CORRECTIVE, // 纠正性 + EMERGENCY // 紧急 + } + + @Column(name = "trigger_type", length = 50) + @Enumerated(EnumType.STRING) + private TriggerType triggerType; + + public enum TriggerType { + PLAN, // 计划触发 + INSPECTION, // 巡检触发 + FAULT, // 故障触发 + MANUAL // 手动创建 + } + + @Column(length = 20) + @Enumerated(EnumType.STRING) + private Priority priority; + + public enum Priority { + LOW, MEDIUM, HIGH, URGENT + } + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private Status status = Status.PENDING; + + public enum Status { + PENDING, // 待分配 + ASSIGNED, // 已分配 + IN_PROGRESS, // 进行中 + COMPLETED, // 已完成 + VERIFIED, // 已验收 + CANCELLED // 已取消 + } + + @Column(length = 200) + private String title; + + @Column(columnDefinition = "TEXT") + private String description; + + @Column(name = "assigned_to", length = 200) + private String assignedTo; + + @Column(name = "assigned_vendor", length = 200) + private String assignedVendor; + + @Column(name = "assigned_date") + private LocalDate assignedDate; + + @Column(name = "actual_start") + private LocalDateTime actualStart; + + @Column(name = "actual_end") + private LocalDateTime actualEnd; + + @Column(name = "actual_hours", precision = 6, scale = 2) + private BigDecimal actualHours; + + @Column(columnDefinition = "TEXT") + private String faultCause; + + @Column(columnDefinition = "TEXT") + private String solution; + + @Column(columnDefinition = "TEXT") + private String result; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "parts_used", columnDefinition = "jsonb") + private List> partsUsed; + + @Column(name = "labor_cost", precision = 12, scale = 2) + private BigDecimal laborCost; + + @Column(name = "parts_cost", precision = 12, scale = 2) + private BigDecimal partsCost; + + @Column(name = "total_cost", precision = 12, scale = 2) + private BigDecimal totalCost; + + @Column(name = "completed_by", length = 200) + private String completedBy; + + @Column(name = "completed_date") + private LocalDate completedDate; + + @Column(name = "verified_by", length = 200) + private String verifiedBy; + + @Column(name = "verified_date") + private LocalDate verifiedDate; + + @Column + private Integer rating; + + @Column + private String remark; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private List photos; + + @Column(columnDefinition = "TEXT") + private String signature; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "created_by", length = 200) + private String createdBy; + + @PrePersist + public void prePersist() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/module-wo/src/main/java/com/ether/pms/ops/entity/WorkOrder.java b/module-wo/src/main/java/com/ether/pms/ops/entity/WorkOrder.java new file mode 100644 index 0000000..3324d68 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/entity/WorkOrder.java @@ -0,0 +1,163 @@ +package com.ether.pms.ops.entity; + +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "ops_work_order") +@Data +public class WorkOrder { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "work_no", unique = true, nullable = false, length = 50) + private String workNo; + + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private Source source; + + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private Type type; + + public enum Source { + OWNER, MAINTENANCE, INSPECTION, FAULT, REGULATORY, MANUAL + } + + public enum Type { + REPAIR, INSPECTION, SECURITY, CLEANING, PROPERTY, CONSULTATION + } + + @Column(nullable = false, length = 20) + @Enumerated(EnumType.STRING) + private Priority priority = Priority.MEDIUM; + + public enum Priority { + LOW, MEDIUM, HIGH, URGENT + } + + @Column(nullable = false, length = 20) + @Enumerated(EnumType.STRING) + private Status status = Status.PENDING; + + public enum Status { + PENDING, ASSIGNED, IN_PROGRESS, COMPLETED, VERIFIED, CANCELLED + } + + @Column(nullable = false, length = 200) + private String title; + + @Column(columnDefinition = "TEXT") + private String description; + + @Column(name = "project_id") + private UUID projectId; + + @Column(name = "equipment_id") + private UUID equipmentId; + + @Column(name = "space_id") + private UUID spaceId; + + @Column(name = "plan_id") + private UUID planId; + + @Column(name = "trigger_type", length = 50) + @Enumerated(EnumType.STRING) + private TriggerType triggerType; + + public enum TriggerType { + PLAN, INSPECTION, FAULT, MANUAL + } + + @Column(name = "assigned_to", length = 200) + private String assignedTo; + + @Column(name = "assigned_vendor", length = 200) + private String assignedVendor; + + @Column(name = "assigned_date") + private LocalDate assignedDate; + + @Column(name = "actual_start") + private LocalDateTime actualStart; + + @Column(name = "actual_end") + private LocalDateTime actualEnd; + + @Column(name = "actual_hours", precision = 6, scale = 2) + private BigDecimal actualHours; + + @Column(name = "fault_cause", columnDefinition = "TEXT") + private String faultCause; + + @Column(columnDefinition = "TEXT") + private String solution; + + @Column(columnDefinition = "TEXT") + private String result; + + @Column(name = "labor_cost", precision = 12, scale = 2) + private BigDecimal laborCost; + + @Column(name = "parts_cost", precision = 12, scale = 2) + private BigDecimal partsCost; + + @Column(name = "total_cost", precision = 12, scale = 2) + private BigDecimal totalCost; + + @Column(name = "completed_by", length = 200) + private String completedBy; + + @Column(name = "completed_date") + private LocalDate completedDate; + + @Column(name = "verified_by", length = 200) + private String verifiedBy; + + @Column(name = "verified_date") + private LocalDate verifiedDate; + + @Column + private Integer rating; + + @Column + private String remark; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(columnDefinition = "jsonb") + private List photos; + + @Column(columnDefinition = "TEXT") + private String signature; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "created_by", length = 200) + private String createdBy; + + @PrePersist + public void prePersist() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/entity/WorkOrderItem.java b/module-wo/src/main/java/com/ether/pms/ops/entity/WorkOrderItem.java new file mode 100644 index 0000000..5e1f526 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/entity/WorkOrderItem.java @@ -0,0 +1,63 @@ +package com.ether.pms.ops.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "ops_work_order_item") +@Data +public class WorkOrderItem { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "work_order_id", nullable = false) + private UUID workOrderId; + + @Column(name = "item_type", nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private ItemType itemType; + + public enum ItemType { + PART, INSPECTION_ITEM, CHECKPOINT + } + + @Column(name = "item_name", nullable = false, length = 200) + private String itemName; + + @Column(precision = 10, scale = 2) + private BigDecimal quantity = BigDecimal.ONE; + + @Column(length = 50) + private String unit; + + @Column(name = "unit_price", precision = 12, scale = 2) + private BigDecimal unitPrice; + + @Column(name = "total_price", precision = 12, scale = 2) + private BigDecimal totalPrice; + + @Column(name = "is_normal") + private Boolean isNormal; + + @Column(columnDefinition = "TEXT") + private String observation; + + @Column(columnDefinition = "TEXT") + private String suggestion; + + @Column(name = "sort_order") + private Integer sortOrder = 0; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @PrePersist + public void prePersist() { + createdAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/repository/InspectionItemRepository.java b/module-wo/src/main/java/com/ether/pms/ops/repository/InspectionItemRepository.java new file mode 100644 index 0000000..ae9107f --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/repository/InspectionItemRepository.java @@ -0,0 +1,18 @@ +package com.ether.pms.ops.repository; + +import com.ether.pms.ops.entity.InspectionItem; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +/** + * 巡检项目数据访问层(工单模块) + */ +@Repository("opsInspectionItemRepository") +public interface InspectionItemRepository extends JpaRepository { + List findByTemplateIdOrderBySortOrder(UUID templateId); + + void deleteByTemplateId(UUID templateId); +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/repository/InspectionTemplateRepository.java b/module-wo/src/main/java/com/ether/pms/ops/repository/InspectionTemplateRepository.java new file mode 100644 index 0000000..5c4b61e --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/repository/InspectionTemplateRepository.java @@ -0,0 +1,22 @@ +package com.ether.pms.ops.repository; + +import com.ether.pms.ops.entity.InspectionTemplate; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +/** + * 巡检模板数据访问层(工单模块) + */ +@Repository("opsInspectionTemplateRepository") +public interface InspectionTemplateRepository extends JpaRepository { + List findByProjectId(UUID projectId); + + List findBySpaceId(UUID spaceId); + + List findByStatus(InspectionTemplate.TemplateStatus status); + + List findByProjectIdAndStatus(UUID projectId, InspectionTemplate.TemplateStatus status); +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/repository/MaintenancePlanRepository.java b/module-wo/src/main/java/com/ether/pms/ops/repository/MaintenancePlanRepository.java new file mode 100644 index 0000000..9f642a4 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/repository/MaintenancePlanRepository.java @@ -0,0 +1,26 @@ +package com.ether.pms.ops.repository; + +import com.ether.pms.ops.entity.MaintenancePlan; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +@Repository +public interface MaintenancePlanRepository extends JpaRepository { + List findByStatus(MaintenancePlan.PlanStatus status); + + List findByEquipmentId(UUID equipmentId); + + List findByProjectId(UUID projectId); + + List findByStatusAndNextDateBefore(MaintenancePlan.PlanStatus status, LocalDate date); + + List findByNextDateBefore(LocalDate date); + + List findByEquipmentIdAndStatus(UUID equipmentId, MaintenancePlan.PlanStatus status); + + List findByEquipmentIdAndPlanType(UUID equipmentId, MaintenancePlan.PlanType planType); +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/repository/MaintenanceTaskRepository.java b/module-wo/src/main/java/com/ether/pms/ops/repository/MaintenanceTaskRepository.java new file mode 100644 index 0000000..0f866f2 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/repository/MaintenanceTaskRepository.java @@ -0,0 +1,124 @@ +package com.ether.pms.ops.repository; + +import com.ether.pms.ops.entity.MaintenanceTask; +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.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * 维保工单Repository + */ +@Repository +public interface MaintenanceTaskRepository extends JpaRepository { + + Optional findByTaskNo(String taskNo); + + List findByEquipmentId(UUID equipmentId); + + List findByPlanId(UUID planId); + + List findByStatus(MaintenanceTask.Status status); + + List findByEquipmentIdAndStatus(UUID equipmentId, MaintenanceTask.Status status); + + List findByPriority(MaintenanceTask.Priority priority); + + List findByAssignedTo(String assignedTo); + + @Query("SELECT t FROM MaintenanceTask t WHERE t.status IN :statuses") + List findByStatusIn(@Param("statuses") List statuses); + + @Query("SELECT t FROM MaintenanceTask t WHERE t.assignedDate < :date AND t.status IN ('PENDING', 'ASSIGNED')") + List findOverdueTasks(@Param("date") LocalDate date); + + boolean existsByTaskNo(String taskNo); + + /** + * 查询指定前缀的最大工单号 + */ + @Query("SELECT MAX(t.taskNo) FROM MaintenanceTask t WHERE t.taskNo LIKE :prefix") + String findMaxTaskNoByDatePrefix(@Param("prefix") String prefix); + + /** + * 统计各状态工单数量 + */ + @Query("SELECT t.status, COUNT(t) FROM MaintenanceTask t GROUP BY t.status") + List countByStatus(); + + /** + * 多条件查询 + */ + @Query("SELECT t FROM MaintenanceTask t WHERE " + + "(:projectId IS NULL OR t.projectId = :projectId) AND " + + "(:status IS NULL OR t.status = :status) AND " + + "(:priority IS NULL OR t.priority = :priority) AND " + + "(:taskType IS NULL OR t.taskType = :taskType) AND " + + "(:assignedTo IS NULL OR t.assignedTo LIKE %:assignedTo%) AND " + + "(:keyword IS NULL OR t.title LIKE %:keyword% OR t.taskNo LIKE %:keyword%)") + List findByConditions( + @Param("projectId") UUID projectId, + @Param("status") MaintenanceTask.Status status, + @Param("priority") MaintenanceTask.Priority priority, + @Param("taskType") MaintenanceTask.TaskType taskType, + @Param("assignedTo") String assignedTo, + @Param("keyword") String keyword); + + /** + * 统计指定状态的工单数量 + */ + @Query("SELECT COUNT(t) FROM MaintenanceTask t WHERE t.status = :status") + long countByStatus(@Param("status") MaintenanceTask.Status status); + + /** + * 统计今日完成的工单数量 + */ + @Query("SELECT COUNT(t) FROM MaintenanceTask t WHERE t.status = 'COMPLETED' AND t.completedDate = :date") + long countCompletedToday(@Param("date") LocalDate date); + + /** + * 统计逾期工单数量 + */ + @Query("SELECT COUNT(t) FROM MaintenanceTask t WHERE t.assignedDate < :date AND t.status IN ('PENDING', 'ASSIGNED')") + long countOverdue(@Param("date") LocalDate date); + + /** + * 计算平均完成工时 + */ + @Query("SELECT AVG(t.actualHours) FROM MaintenanceTask t WHERE t.status IN ('COMPLETED', 'VERIFIED') AND t.actualHours IS NOT NULL") + Double calculateAvgCompleteHours(); + + /** + * 计算平均评分 + */ + @Query("SELECT AVG(t.rating) FROM MaintenanceTask t WHERE t.rating IS NOT NULL") + Double calculateAvgRating(); + + /** + * 按优先级统计工单数量 + */ + @Query("SELECT t.priority, COUNT(t) FROM MaintenanceTask t GROUP BY t.priority") + List countByPriority(); + + /** + * 按触发类型统计工单数量 + */ + @Query("SELECT t.triggerType, COUNT(t) FROM MaintenanceTask t GROUP BY t.triggerType") + List countByTriggerType(); + + /** + * 统计指定日期范围内创建的工单数量 + */ + @Query("SELECT COUNT(t) FROM MaintenanceTask t WHERE t.createdAt >= :startDate AND t.createdAt < :endDate") + long countByCreatedAtBetween(@Param("startDate") java.time.LocalDateTime startDate, @Param("endDate") java.time.LocalDateTime endDate); + + /** + * 查询指定计划在指定时间范围内创建的工单 + */ + List findByPlanIdAndCreatedAtBetween(UUID planId, java.time.LocalDateTime startDate, java.time.LocalDateTime endDate); +} diff --git a/module-wo/src/main/java/com/ether/pms/ops/repository/WorkOrderItemRepository.java b/module-wo/src/main/java/com/ether/pms/ops/repository/WorkOrderItemRepository.java new file mode 100644 index 0000000..0c16f10 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/repository/WorkOrderItemRepository.java @@ -0,0 +1,15 @@ +package com.ether.pms.ops.repository; + +import com.ether.pms.ops.entity.WorkOrderItem; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.UUID; + +@Repository +public interface WorkOrderItemRepository extends JpaRepository { + List findByWorkOrderIdOrderBySortOrder(UUID workOrderId); + + void deleteByWorkOrderId(UUID workOrderId); +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/repository/WorkOrderRepository.java b/module-wo/src/main/java/com/ether/pms/ops/repository/WorkOrderRepository.java new file mode 100644 index 0000000..c8209df --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/repository/WorkOrderRepository.java @@ -0,0 +1,63 @@ +package com.ether.pms.ops.repository; + +import com.ether.pms.ops.entity.WorkOrder; +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.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface WorkOrderRepository extends JpaRepository { + Optional findByWorkNo(String workNo); + + List findByProjectId(UUID projectId); + + List findByEquipmentId(UUID equipmentId); + + List findBySource(WorkOrder.Source source); + + List findByType(WorkOrder.Type type); + + List findByStatus(WorkOrder.Status status); + + List findByAssignedTo(String assignedTo); + + @Query("SELECT w FROM WorkOrder w WHERE w.assignedDate < :date AND w.status IN ('PENDING', 'ASSIGNED')") + List findOverdueTasks(@Param("date") LocalDate date); + + @Query("SELECT MAX(w.workNo) FROM WorkOrder w WHERE w.workNo LIKE :prefix") + String findMaxWorkNoByPrefix(@Param("prefix") String prefix); + + long countByStatus(WorkOrder.Status status); + + @Query("SELECT COUNT(w) FROM WorkOrder w WHERE w.status = 'COMPLETED' AND w.completedDate = :date") + long countCompletedToday(@Param("date") LocalDate date); + + @Query("SELECT COUNT(w) FROM WorkOrder w WHERE w.assignedDate < :date AND w.status IN ('PENDING', 'ASSIGNED')") + long countOverdue(@Param("date") LocalDate date); + + @Query("SELECT w.status, COUNT(w) FROM WorkOrder w GROUP BY w.status") + List countByStatus(); + + @Query("SELECT w.source, COUNT(w) FROM WorkOrder w GROUP BY w.source") + List countBySource(); + + @Query("SELECT w.type, COUNT(w) FROM WorkOrder w GROUP BY w.type") + List countByType(); + + @Query("SELECT w.priority, COUNT(w) FROM WorkOrder w GROUP BY w.priority") + List countByPriority(); + + @Query("SELECT COUNT(w) FROM WorkOrder w WHERE w.createdAt >= :startDate AND w.createdAt < :endDate") + long countByCreatedAtBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); + + List findByPlanId(UUID planId); + + List findByPlanIdAndCreatedAtBetween(UUID planId, LocalDateTime startDate, LocalDateTime endDate); +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/scheduler/MaintenanceScheduler.java b/module-wo/src/main/java/com/ether/pms/ops/scheduler/MaintenanceScheduler.java new file mode 100644 index 0000000..bab7f04 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/scheduler/MaintenanceScheduler.java @@ -0,0 +1,179 @@ +package com.ether.pms.ops.scheduler; + +import com.ether.pms.ops.entity.MaintenancePlan; +import com.ether.pms.ops.entity.WorkOrder; +import com.ether.pms.ops.repository.MaintenancePlanRepository; +import com.ether.pms.ops.repository.WorkOrderRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * 维保计划定时调度器 + * 每天凌晨1点检查到期的维保计划,自动生成工单 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class MaintenanceScheduler { + + private final MaintenancePlanRepository maintenancePlanRepository; + private final WorkOrderRepository workOrderRepository; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + /** + * 从维保计划自动生成工单 + * 每天凌晨1点执行 + */ + @Scheduled(cron = "0 0 1 * * ?") + @Transactional + public void generateTasksFromPlans() { + log.info("开始执行维保计划自动生成工单任务..."); + + try { + LocalDate today = LocalDate.now(); + + // 查询所有ACTIVE状态且下次维保日期已到的计划 + List duePlans = maintenancePlanRepository.findByStatusAndNextDateBefore( + MaintenancePlan.PlanStatus.ACTIVE, today.plusDays(1)); + + int generatedCount = 0; + + for (MaintenancePlan plan : duePlans) { + try { + // 检查今天是否已生成过工单 + if (isTaskGeneratedToday(plan.getId(), today)) { + log.info("计划[{}]今天已生成过工单,跳过", plan.getPlanCode()); + continue; + } + + // 生成工单 + WorkOrder task = createTaskFromPlan(plan); + workOrderRepository.save(task); + + // 更新计划下次日期 + updatePlanNextDate(plan); + + generatedCount++; + log.info("为计划[{}]生成工单[{}]成功", plan.getPlanCode(), task.getWorkNo()); + + } catch (Exception e) { + log.error("为计划[{}]生成工单失败: {}", plan.getPlanCode(), e.getMessage(), e); + } + } + + log.info("维保计划自动生成工单任务完成,共生成 {} 个工单", generatedCount); + + } catch (Exception e) { + log.error("维保计划自动生成工单任务失败: {}", e.getMessage(), e); + } + } + + /** + * 检查逾期任务 + * 每小时执行 + */ + @Scheduled(cron = "0 0 * * * ?") + public void checkOverdueTasks() { + log.info("开始检查逾期工单..."); + try { + LocalDate today = LocalDate.now(); + List overdueTasks = workOrderRepository.findOverdueTasks(today); + + if (!overdueTasks.isEmpty()) { + log.warn("发现 {} 个逾期工单", overdueTasks.size()); + for (WorkOrder task : overdueTasks) { + log.warn("逾期工单: {} - {} - 负责人: {}", + task.getWorkNo(), task.getTitle(), task.getAssignedTo()); + } + } else { + log.info("未发现逾期工单"); + } + } catch (Exception e) { + log.error("检查逾期工单失败: {}", e.getMessage(), e); + } + } + + /** + * 检查今天是否已为该计划生成过工单 + */ + private boolean isTaskGeneratedToday(java.util.UUID planId, LocalDate today) { + LocalDateTime todayStart = today.atStartOfDay(); + LocalDateTime tomorrowStart = today.plusDays(1).atStartOfDay(); + + List todayTasks = workOrderRepository.findByPlanIdAndCreatedAtBetween( + planId, todayStart, tomorrowStart); + + return !todayTasks.isEmpty(); + } + + /** + * 从计划创建工单 + */ + private WorkOrder createTaskFromPlan(MaintenancePlan plan) { + WorkOrder task = new WorkOrder(); + task.setPlanId(plan.getId()); + task.setEquipmentId(plan.getEquipmentId()); + task.setProjectId(plan.getProjectId()); + task.setSource(WorkOrder.Source.MAINTENANCE); + task.setType(WorkOrder.Type.REPAIR); + task.setTriggerType(WorkOrder.TriggerType.PLAN); + task.setTitle(plan.getPlanName()); + task.setDescription(plan.getPlanContent()); + task.setPriority(WorkOrder.Priority.MEDIUM); + task.setStatus(WorkOrder.Status.PENDING); + task.setAssignedVendor(plan.getAssignedVendor()); + + // 生成工单编号 + task.setWorkNo(generateWorkNo()); + + // 预计工时 + if (plan.getEstimatedHours() != null) { + task.setActualHours(plan.getEstimatedHours()); + } + + return task; + } + + /** + * 生成工单编号 WO-YYYYMMDD-XXXX + */ + private String generateWorkNo() { + String dateStr = LocalDate.now().format(DATE_FORMATTER); + String prefix = "WO-" + dateStr + "-"; + + String maxWorkNo = workOrderRepository.findMaxWorkNoByPrefix(prefix + "%"); + + int sequence = 1; + if (maxWorkNo != null) { + try { + String seqStr = maxWorkNo.substring(maxWorkNo.lastIndexOf("-") + 1); + sequence = Integer.parseInt(seqStr) + 1; + } catch (Exception e) { + sequence = 1; + } + } + + return String.format("%s%04d", prefix, sequence); + } + + /** + * 更新计划的下次维保日期 + */ + private void updatePlanNextDate(MaintenancePlan plan) { + if (plan.getCycleDays() != null && plan.getCycleDays() > 0) { + LocalDate newNextDate = LocalDate.now().plusDays(plan.getCycleDays()); + plan.setLastDate(LocalDate.now()); + plan.setNextDate(newNextDate); + maintenancePlanRepository.save(plan); + log.info("更新计划[{}]下次维保日期为: {}", plan.getPlanCode(), newNextDate); + } + } +} diff --git a/module-wo/src/main/java/com/ether/pms/ops/service/MaintenancePlanService.java b/module-wo/src/main/java/com/ether/pms/ops/service/MaintenancePlanService.java new file mode 100644 index 0000000..3891f2e --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/service/MaintenancePlanService.java @@ -0,0 +1,33 @@ +package com.ether.pms.ops.service; + +import com.ether.pms.ops.entity.MaintenancePlan; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * 维保计划服务接口 + */ +public interface MaintenancePlanService { + + MaintenancePlan createPlan(MaintenancePlan plan); + + MaintenancePlan updatePlan(UUID id, MaintenancePlan plan); + + void deletePlan(UUID id); + + MaintenancePlan getPlanById(UUID id); + + List getAllPlans(); + + List getPlansByEquipment(UUID equipmentId); + + List getPlansByEquipmentAndStatus(UUID equipmentId, MaintenancePlan.PlanStatus status); + + List getPlansByStatus(MaintenancePlan.PlanStatus status); + + List getPlansByNextDateBefore(LocalDate date); + + List getPlansByEquipmentAndPlanType(UUID equipmentId, MaintenancePlan.PlanType planType); +} diff --git a/module-wo/src/main/java/com/ether/pms/ops/service/MaintenanceTaskService.java b/module-wo/src/main/java/com/ether/pms/ops/service/MaintenanceTaskService.java new file mode 100644 index 0000000..376ee8e --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/service/MaintenanceTaskService.java @@ -0,0 +1,62 @@ +package com.ether.pms.ops.service; + +import com.ether.pms.ops.dto.MaintenanceTaskStatsDTO; +import com.ether.pms.ops.entity.MaintenanceTask; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * 维保工单服务接口 + */ +public interface MaintenanceTaskService { + + MaintenanceTask createTask(MaintenanceTask task); + + MaintenanceTask updateTask(UUID id, MaintenanceTask task); + + void deleteTask(UUID id); + + MaintenanceTask getTaskById(UUID id); + + List getAllTasks(); + + List getTasksByEquipment(UUID equipmentId); + + List getTasksByPlan(UUID planId); + + List getTasksByStatus(MaintenanceTask.Status status); + + List getTasksByPriority(MaintenanceTask.Priority priority); + + List getTasksByAssignedTo(String assignedTo); + + List getOverdueTasks(LocalDate date); + + MaintenanceTask assignTask(UUID id, String assignedTo, LocalDate assignedDate); + + MaintenanceTask startTask(UUID id); + + MaintenanceTask completeTask(UUID id, String result, BigDecimal actualHours, BigDecimal cost, String completedBy); + + /** + * 完成工单(带详细信息) + */ + MaintenanceTask completeTaskWithDetails(UUID id, MaintenanceTask taskData); + + /** + * 验收工单 + */ + MaintenanceTask verifyTask(UUID id, String verifiedBy, String remark, Integer rating); + + MaintenanceTask cancelTask(UUID id); + + MaintenanceTask rateTask(UUID id, Integer rating); + + /** + * 获取工单统计信息 + */ + MaintenanceTaskStatsDTO getTaskStats(); +} diff --git a/module-wo/src/main/java/com/ether/pms/ops/service/WorkOrderService.java b/module-wo/src/main/java/com/ether/pms/ops/service/WorkOrderService.java new file mode 100644 index 0000000..38bb7cd --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/service/WorkOrderService.java @@ -0,0 +1,37 @@ +package com.ether.pms.ops.service; + +import com.ether.pms.ops.dto.WorkOrderStatsDTO; +import com.ether.pms.ops.entity.WorkOrder; +import com.ether.pms.ops.entity.WorkOrderItem; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +public interface WorkOrderService { + WorkOrder createWorkOrder(WorkOrder workOrder); + WorkOrder updateWorkOrder(UUID id, WorkOrder workOrder); + void deleteWorkOrder(UUID id); + WorkOrder getWorkOrderById(UUID id); + List getAllWorkOrders(); + List getWorkOrdersByProject(UUID projectId); + List getWorkOrdersByEquipment(UUID equipmentId); + List getWorkOrdersBySource(WorkOrder.Source source); + List getWorkOrdersByType(WorkOrder.Type type); + List getWorkOrdersByStatus(WorkOrder.Status status); + List getWorkOrdersByAssignedTo(String assignedTo); + List getOverdueWorkOrders(LocalDate date); + + WorkOrder assignWorkOrder(UUID id, String assignedTo, String assignedVendor, LocalDate assignedDate); + WorkOrder startWorkOrder(UUID id); + WorkOrder completeWorkOrder(UUID id, WorkOrder workOrderData); + WorkOrder verifyWorkOrder(UUID id, String verifiedBy, String remark, Integer rating); + WorkOrder cancelWorkOrder(UUID id); + + WorkOrderStatsDTO getWorkOrderStats(); + + List getWorkOrderItems(UUID workOrderId); + WorkOrder addWorkOrderItem(UUID workOrderId, WorkOrderItem item); + WorkOrder addWorkOrderItems(UUID workOrderId, List items); +} \ No newline at end of file diff --git a/module-wo/src/main/java/com/ether/pms/ops/service/impl/MaintenancePlanServiceImpl.java b/module-wo/src/main/java/com/ether/pms/ops/service/impl/MaintenancePlanServiceImpl.java new file mode 100644 index 0000000..aa02073 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/service/impl/MaintenancePlanServiceImpl.java @@ -0,0 +1,110 @@ +package com.ether.pms.ops.service.impl; + +import com.ether.pms.common.BusinessException; +import com.ether.pms.common.ErrorCode; +import com.ether.pms.ops.entity.MaintenancePlan; +import com.ether.pms.ops.repository.MaintenancePlanRepository; +import com.ether.pms.ops.service.MaintenancePlanService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +/** + * 维保计划服务实现 + */ +@Service +@RequiredArgsConstructor +public class MaintenancePlanServiceImpl implements MaintenancePlanService { + + private final MaintenancePlanRepository maintenancePlanRepository; + + @Override + @Transactional + public MaintenancePlan createPlan(MaintenancePlan plan) { + if (plan.getStatus() == null) { + plan.setStatus(MaintenancePlan.PlanStatus.ACTIVE); + } + return maintenancePlanRepository.save(plan); + } + + @Override + @Transactional + public MaintenancePlan updatePlan(UUID id, MaintenancePlan plan) { + MaintenancePlan existing = getPlanById(id); + + if (plan.getPlanName() != null) { + existing.setPlanName(plan.getPlanName()); + } + if (plan.getPlanType() != null) { + existing.setPlanType(plan.getPlanType()); + } + if (plan.getCycleDays() != null) { + existing.setCycleDays(plan.getCycleDays()); + } + if (plan.getLastDate() != null) { + existing.setLastDate(plan.getLastDate()); + } + if (plan.getNextDate() != null) { + existing.setNextDate(plan.getNextDate()); + } + if (plan.getEstimatedHours() != null) { + existing.setEstimatedHours(plan.getEstimatedHours()); + } + if (plan.getAssignedVendor() != null) { + existing.setAssignedVendor(plan.getAssignedVendor()); + } + if (plan.getStatus() != null) { + existing.setStatus(plan.getStatus()); + } + + return maintenancePlanRepository.save(existing); + } + + @Override + @Transactional + public void deletePlan(UUID id) { + MaintenancePlan plan = getPlanById(id); + plan.setStatus(MaintenancePlan.PlanStatus.INACTIVE); + maintenancePlanRepository.save(plan); + } + + @Override + public MaintenancePlan getPlanById(UUID id) { + return maintenancePlanRepository.findById(id) + .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "维保计划不存在")); + } + + @Override + public List getAllPlans() { + return maintenancePlanRepository.findAll(); + } + + @Override + public List getPlansByEquipment(UUID equipmentId) { + return maintenancePlanRepository.findByEquipmentId(equipmentId); + } + + @Override + public List getPlansByEquipmentAndStatus(UUID equipmentId, MaintenancePlan.PlanStatus status) { + return maintenancePlanRepository.findByEquipmentIdAndStatus(equipmentId, status); + } + + @Override + public List getPlansByStatus(MaintenancePlan.PlanStatus status) { + return maintenancePlanRepository.findByStatus(status); + } + + @Override + public List getPlansByNextDateBefore(LocalDate date) { + return maintenancePlanRepository.findByNextDateBefore(date); + } + + @Override + public List getPlansByEquipmentAndPlanType(UUID equipmentId, MaintenancePlan.PlanType planType) { + return maintenancePlanRepository.findByEquipmentIdAndPlanType(equipmentId, planType); + } +} diff --git a/module-wo/src/main/java/com/ether/pms/ops/service/impl/MaintenanceTaskServiceImpl.java b/module-wo/src/main/java/com/ether/pms/ops/service/impl/MaintenanceTaskServiceImpl.java new file mode 100644 index 0000000..f631ca6 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/service/impl/MaintenanceTaskServiceImpl.java @@ -0,0 +1,506 @@ +package com.ether.pms.ops.service.impl; + +import com.ether.pms.common.BusinessException; +import com.ether.pms.common.ErrorCode; +import com.ether.pms.asset.entity.Equipment; +import com.ether.pms.asset.repository.EquipmentRepository; +import com.ether.pms.ops.dto.MaintenanceTaskStatsDTO; +import com.ether.pms.ops.entity.MaintenanceTask; +import com.ether.pms.ops.repository.MaintenanceTaskRepository; +import com.ether.pms.ops.service.MaintenanceTaskService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * 维保工单服务实现 + */ +@Service +@RequiredArgsConstructor +public class MaintenanceTaskServiceImpl implements MaintenanceTaskService { + + private final MaintenanceTaskRepository maintenanceTaskRepository; + private final EquipmentRepository equipmentRepository; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + @Override + @Transactional + public MaintenanceTask createTask(MaintenanceTask task) { + if (task.getTaskNo() == null || task.getTaskNo().isEmpty()) { + task.setTaskNo(generateTaskNo()); + } + if (task.getStatus() == null) { + task.setStatus(MaintenanceTask.Status.PENDING); + } + // 自动判定优先级 + if (task.getPriority() == null) { + task.setPriority(autoDeterminePriority(task)); + } + // 自动计算总费用 + calculateTotalCost(task); + return maintenanceTaskRepository.save(task); + } + + /** + * 自动判定工单优先级 + * 规则: + * 1. 紧急维修 → URGENT + * 2. 故障触发 + 关键词(困人/漏水/停电/火灾)→ URGENT + * 3. 故障触发 → HIGH + * 4. 计划触发 → MEDIUM + * 5. 其他 → MEDIUM + */ + private MaintenanceTask.Priority autoDeterminePriority(MaintenanceTask task) { + // 1. 紧急维修类型 + if (task.getTaskType() == MaintenanceTask.TaskType.EMERGENCY) { + return MaintenanceTask.Priority.URGENT; + } + + // 2. 故障触发类型 + if (task.getTriggerType() == MaintenanceTask.TriggerType.FAULT) { + // 检查标题和描述中的紧急关键词 + String content = (task.getTitle() != null ? task.getTitle() : "") + + (task.getDescription() != null ? task.getDescription() : ""); + content = content.toLowerCase(); + + // 紧急关键词列表 + String[] urgentKeywords = {"困人", "漏水", "停电", "火灾", "爆炸", "漏电", "冒烟", "故障停机"}; + for (String keyword : urgentKeywords) { + if (content.contains(keyword)) { + return MaintenanceTask.Priority.URGENT; + } + } + return MaintenanceTask.Priority.HIGH; + } + + // 3. 巡检触发 + if (task.getTriggerType() == MaintenanceTask.TriggerType.INSPECTION) { + return MaintenanceTask.Priority.HIGH; + } + + // 4. 计划触发 + if (task.getTriggerType() == MaintenanceTask.TriggerType.PLAN) { + return MaintenanceTask.Priority.MEDIUM; + } + + // 5. 默认 + return MaintenanceTask.Priority.MEDIUM; + } + + @Override + @Transactional + public MaintenanceTask updateTask(UUID id, MaintenanceTask task) { + MaintenanceTask existing = getTaskById(id); + + if (task.getTitle() != null) { + existing.setTitle(task.getTitle()); + } + if (task.getDescription() != null) { + existing.setDescription(task.getDescription()); + } + if (task.getTaskType() != null) { + existing.setTaskType(task.getTaskType()); + } + if (task.getTriggerType() != null) { + existing.setTriggerType(task.getTriggerType()); + } + if (task.getPriority() != null) { + existing.setPriority(task.getPriority()); + } + if (task.getAssignedTo() != null) { + existing.setAssignedTo(task.getAssignedTo()); + } + if (task.getAssignedVendor() != null) { + existing.setAssignedVendor(task.getAssignedVendor()); + } + if (task.getAssignedDate() != null) { + existing.setAssignedDate(task.getAssignedDate()); + } + if (task.getPartsUsed() != null) { + existing.setPartsUsed(task.getPartsUsed()); + } + if (task.getLaborCost() != null) { + existing.setLaborCost(task.getLaborCost()); + } + if (task.getPartsCost() != null) { + existing.setPartsCost(task.getPartsCost()); + } + if (task.getFaultCause() != null) { + existing.setFaultCause(task.getFaultCause()); + } + if (task.getSolution() != null) { + existing.setSolution(task.getSolution()); + } + if (task.getResult() != null) { + existing.setResult(task.getResult()); + } + if (task.getRemark() != null) { + existing.setRemark(task.getRemark()); + } + if (task.getPhotos() != null) { + existing.setPhotos(task.getPhotos()); + } + if (task.getSignature() != null) { + existing.setSignature(task.getSignature()); + } + + // 重新计算总费用 + calculateTotalCost(existing); + + return maintenanceTaskRepository.save(existing); + } + + @Override + @Transactional + public void deleteTask(UUID id) { + MaintenanceTask task = getTaskById(id); + task.setStatus(MaintenanceTask.Status.CANCELLED); + maintenanceTaskRepository.save(task); + } + + @Override + public MaintenanceTask getTaskById(UUID id) { + return maintenanceTaskRepository.findById(id) + .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "维保工单不存在")); + } + + @Override + public List getAllTasks() { + return maintenanceTaskRepository.findAll(); + } + + @Override + public List getTasksByEquipment(UUID equipmentId) { + return maintenanceTaskRepository.findByEquipmentId(equipmentId); + } + + @Override + public List getTasksByPlan(UUID planId) { + return maintenanceTaskRepository.findByPlanId(planId); + } + + @Override + public List getTasksByStatus(MaintenanceTask.Status status) { + return maintenanceTaskRepository.findByStatus(status); + } + + @Override + public List getTasksByPriority(MaintenanceTask.Priority priority) { + return maintenanceTaskRepository.findByPriority(priority); + } + + @Override + public List getTasksByAssignedTo(String assignedTo) { + return maintenanceTaskRepository.findByAssignedTo(assignedTo); + } + + @Override + public List getOverdueTasks(LocalDate date) { + return maintenanceTaskRepository.findOverdueTasks(date); + } + + @Override + @Transactional + public MaintenanceTask assignTask(UUID id, String assignedTo, LocalDate assignedDate) { + MaintenanceTask task = getTaskById(id); + + if (task.getStatus() != MaintenanceTask.Status.PENDING) { + throw new BusinessException(6001, "只有PENDING状态的工单才能分配"); + } + + task.setStatus(MaintenanceTask.Status.ASSIGNED); + task.setAssignedTo(assignedTo); + task.setAssignedDate(assignedDate != null ? assignedDate : LocalDate.now()); + + return maintenanceTaskRepository.save(task); + } + + @Override + @Transactional + public MaintenanceTask startTask(UUID id) { + MaintenanceTask task = getTaskById(id); + + if (task.getStatus() != MaintenanceTask.Status.ASSIGNED) { + throw new BusinessException(6002, "只有ASSIGNED状态的工单才能开始执行"); + } + + task.setStatus(MaintenanceTask.Status.IN_PROGRESS); + task.setActualStart(LocalDateTime.now()); + + return maintenanceTaskRepository.save(task); + } + + @Override + @Transactional + public MaintenanceTask completeTask(UUID id, String result, BigDecimal actualHours, BigDecimal cost, String completedBy) { + MaintenanceTask task = getTaskById(id); + + if (task.getStatus() != MaintenanceTask.Status.IN_PROGRESS) { + throw new BusinessException(6003, "只有IN_PROGRESS状态的工单才能完成"); + } + + task.setStatus(MaintenanceTask.Status.COMPLETED); + task.setActualEnd(LocalDateTime.now()); + task.setResult(result); + if (actualHours != null) { + task.setActualHours(actualHours); + } + if (cost != null) { + task.setTotalCost(cost); + } + task.setCompletedBy(completedBy); + task.setCompletedDate(LocalDate.now()); + + return maintenanceTaskRepository.save(task); + } + + @Override + @Transactional + public MaintenanceTask completeTaskWithDetails(UUID id, MaintenanceTask taskData) { + MaintenanceTask task = getTaskById(id); + + if (task.getStatus() != MaintenanceTask.Status.IN_PROGRESS) { + throw new BusinessException(6003, "只有IN_PROGRESS状态的工单才能完成"); + } + + task.setStatus(MaintenanceTask.Status.COMPLETED); + task.setActualEnd(LocalDateTime.now()); + + // 更新执行信息 + if (taskData.getActualHours() != null) { + task.setActualHours(taskData.getActualHours()); + } + if (taskData.getFaultCause() != null) { + task.setFaultCause(taskData.getFaultCause()); + } + if (taskData.getSolution() != null) { + task.setSolution(taskData.getSolution()); + } + if (taskData.getResult() != null) { + task.setResult(taskData.getResult()); + } + if (taskData.getPartsUsed() != null) { + task.setPartsUsed(taskData.getPartsUsed()); + } + if (taskData.getLaborCost() != null) { + task.setLaborCost(taskData.getLaborCost()); + } + if (taskData.getPartsCost() != null) { + task.setPartsCost(taskData.getPartsCost()); + } + if (taskData.getPhotos() != null) { + task.setPhotos(taskData.getPhotos()); + } + if (taskData.getSignature() != null) { + task.setSignature(taskData.getSignature()); + } + if (taskData.getCompletedBy() != null) { + task.setCompletedBy(taskData.getCompletedBy()); + } + + // 计算总费用 + calculateTotalCost(task); + task.setCompletedDate(LocalDate.now()); + + // 保存工单 + MaintenanceTask savedTask = maintenanceTaskRepository.save(task); + + // 更新设备维保记录 + updateEquipmentMaintenanceRecord(savedTask); + + return savedTask; + } + + /** + * 更新设备维保记录 + */ + private void updateEquipmentMaintenanceRecord(MaintenanceTask task) { + try { + Equipment equipment = equipmentRepository.findById(task.getEquipmentId()).orElse(null); + if (equipment == null) { + return; + } + + // 更新维保商信息 + if (task.getAssignedVendor() != null && !task.getAssignedVendor().isEmpty()) { + equipment.setMaintenanceVendor(task.getAssignedVendor()); + } + + // 更新下次巡检日期(如果工单是预防性维护) + if (task.getTaskType() == MaintenanceTask.TaskType.PREVENTIVE) { + // 根据设备类型设置不同的巡检周期,这里默认30天 + int inspectionCycle = equipment.getInspectionCycle() != null ? equipment.getInspectionCycle() : 30; + equipment.setNextInspectionDate(LocalDate.now().plusDays(inspectionCycle)); + } + + equipmentRepository.save(equipment); + } catch (Exception e) { + // 记录日志但不影响工单完成 + System.err.println("更新设备维保记录失败: " + e.getMessage()); + } + } + + @Override + @Transactional + public MaintenanceTask verifyTask(UUID id, String verifiedBy, String remark, Integer rating) { + MaintenanceTask task = getTaskById(id); + + if (task.getStatus() != MaintenanceTask.Status.COMPLETED) { + throw new BusinessException(6007, "只有COMPLETED状态的工单才能验收"); + } + + task.setStatus(MaintenanceTask.Status.VERIFIED); + task.setVerifiedBy(verifiedBy); + task.setVerifiedDate(LocalDate.now()); + task.setRemark(remark); + if (rating != null && rating >= 1 && rating <= 5) { + task.setRating(rating); + } + + return maintenanceTaskRepository.save(task); + } + + @Override + @Transactional + public MaintenanceTask cancelTask(UUID id) { + MaintenanceTask task = getTaskById(id); + + List cancellableStatuses = Arrays.asList( + MaintenanceTask.Status.PENDING, + MaintenanceTask.Status.ASSIGNED, + MaintenanceTask.Status.IN_PROGRESS + ); + + if (!cancellableStatuses.contains(task.getStatus())) { + throw new BusinessException(6004, "只有PENDING、ASSIGNED或IN_PROGRESS状态的工单才能取消"); + } + + task.setStatus(MaintenanceTask.Status.CANCELLED); + + return maintenanceTaskRepository.save(task); + } + + @Override + @Transactional + public MaintenanceTask rateTask(UUID id, Integer rating) { + MaintenanceTask task = getTaskById(id); + + if (task.getStatus() != MaintenanceTask.Status.COMPLETED && task.getStatus() != MaintenanceTask.Status.VERIFIED) { + throw new BusinessException(6005, "只有已完成或已验收的工单才能评分"); + } + + if (rating < 1 || rating > 5) { + throw new BusinessException(6006, "评分必须在1-5之间"); + } + + task.setRating(rating); + + return maintenanceTaskRepository.save(task); + } + + /** + * 生成工单编号:EQ-YYYYMMDD-XXXX + * 格式:EQ-20260328-0001 + */ + private String generateTaskNo() { + String dateStr = LocalDate.now().format(DATE_FORMATTER); + String prefix = "EQ-" + dateStr + "-"; + + // 查询当天最大的工单号 + String maxTaskNo = maintenanceTaskRepository.findMaxTaskNoByDatePrefix(prefix + "%"); + + int sequence = 1; + if (maxTaskNo != null) { + try { + String seqStr = maxTaskNo.substring(maxTaskNo.lastIndexOf("-") + 1); + sequence = Integer.parseInt(seqStr) + 1; + } catch (Exception e) { + sequence = 1; + } + } + + return String.format("%s%04d", prefix, sequence); + } + + /** + * 计算总费用 + */ + private void calculateTotalCost(MaintenanceTask task) { + BigDecimal laborCost = task.getLaborCost() != null ? task.getLaborCost() : BigDecimal.ZERO; + BigDecimal partsCost = task.getPartsCost() != null ? task.getPartsCost() : BigDecimal.ZERO; + task.setTotalCost(laborCost.add(partsCost)); + } + + @Override + public MaintenanceTaskStatsDTO getTaskStats() { + LocalDate today = LocalDate.now(); + LocalDateTime todayStart = today.atStartOfDay(); + LocalDateTime tomorrowStart = today.plusDays(1).atStartOfDay(); + + // 基础统计 + long total = maintenanceTaskRepository.count(); + long pending = maintenanceTaskRepository.countByStatus(MaintenanceTask.Status.PENDING); + long assigned = maintenanceTaskRepository.countByStatus(MaintenanceTask.Status.ASSIGNED); + long inProgress = maintenanceTaskRepository.countByStatus(MaintenanceTask.Status.IN_PROGRESS); + long completed = maintenanceTaskRepository.countByStatus(MaintenanceTask.Status.COMPLETED); + long verified = maintenanceTaskRepository.countByStatus(MaintenanceTask.Status.VERIFIED); + long cancelled = maintenanceTaskRepository.countByStatus(MaintenanceTask.Status.CANCELLED); + + // 今日统计 + long completedToday = maintenanceTaskRepository.countCompletedToday(today); + long createdToday = maintenanceTaskRepository.countByCreatedAtBetween(todayStart, tomorrowStart); + + // 逾期统计 + long overdue = maintenanceTaskRepository.countOverdue(today); + + // 效率指标 + Double avgHours = maintenanceTaskRepository.calculateAvgCompleteHours(); + Double avgRating = maintenanceTaskRepository.calculateAvgRating(); + + // 按优先级统计 + Map byPriority = new HashMap<>(); + List priorityStats = maintenanceTaskRepository.countByPriority(); + for (Object[] stat : priorityStats) { + if (stat[0] != null) { + byPriority.put(stat[0].toString(), (Long) stat[1]); + } + } + + // 按触发类型统计 + Map byTriggerType = new HashMap<>(); + List triggerStats = maintenanceTaskRepository.countByTriggerType(); + for (Object[] stat : triggerStats) { + if (stat[0] != null) { + byTriggerType.put(stat[0].toString(), (Long) stat[1]); + } + } + + return MaintenanceTaskStatsDTO.builder() + .total(total) + .pending(pending) + .assigned(assigned) + .inProgress(inProgress) + .completed(completed) + .verified(verified) + .cancelled(cancelled) + .completedToday(completedToday) + .createdToday(createdToday) + .overdue(overdue) + .avgCompleteHours(avgHours != null ? BigDecimal.valueOf(avgHours).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO) + .avgRating(avgRating != null ? BigDecimal.valueOf(avgRating).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO) + .byPriority(byPriority) + .byTriggerType(byTriggerType) + .build(); + } +} diff --git a/module-wo/src/main/java/com/ether/pms/ops/service/impl/WorkOrderServiceImpl.java b/module-wo/src/main/java/com/ether/pms/ops/service/impl/WorkOrderServiceImpl.java new file mode 100644 index 0000000..17e3b27 --- /dev/null +++ b/module-wo/src/main/java/com/ether/pms/ops/service/impl/WorkOrderServiceImpl.java @@ -0,0 +1,330 @@ +package com.ether.pms.ops.service.impl; + +import com.ether.pms.ops.dto.WorkOrderStatsDTO; +import com.ether.pms.ops.entity.WorkOrder; +import com.ether.pms.ops.entity.WorkOrderItem; +import com.ether.pms.ops.repository.WorkOrderItemRepository; +import com.ether.pms.ops.repository.WorkOrderRepository; +import com.ether.pms.ops.service.WorkOrderService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class WorkOrderServiceImpl implements WorkOrderService { + + private final WorkOrderRepository workOrderRepository; + private final WorkOrderItemRepository workOrderItemRepository; + + @Override + @Transactional + public WorkOrder createWorkOrder(WorkOrder workOrder) { + // 生成工单编号 + String workNo = generateWorkNo(); + workOrder.setWorkNo(workNo); + + // 设置默认状态 + if (workOrder.getStatus() == null) { + workOrder.setStatus(WorkOrder.Status.PENDING); + } + if (workOrder.getPriority() == null) { + workOrder.setPriority(WorkOrder.Priority.MEDIUM); + } + + return workOrderRepository.save(workOrder); + } + + @Override + @Transactional + public WorkOrder updateWorkOrder(UUID id, WorkOrder workOrder) { + WorkOrder existing = getWorkOrderById(id); + + // 更新基本信息 + existing.setTitle(workOrder.getTitle()); + existing.setDescription(workOrder.getDescription()); + existing.setSource(workOrder.getSource()); + existing.setType(workOrder.getType()); + existing.setPriority(workOrder.getPriority()); + existing.setProjectId(workOrder.getProjectId()); + existing.setEquipmentId(workOrder.getEquipmentId()); + existing.setSpaceId(workOrder.getSpaceId()); + + // 更新费用信息 + existing.setLaborCost(workOrder.getLaborCost()); + existing.setPartsCost(workOrder.getPartsCost()); + existing.setTotalCost(workOrder.getTotalCost()); + + return workOrderRepository.save(existing); + } + + @Override + @Transactional + public void deleteWorkOrder(UUID id) { + workOrderRepository.deleteById(id); + } + + @Override + public WorkOrder getWorkOrderById(UUID id) { + return workOrderRepository.findById(id) + .orElseThrow(() -> new RuntimeException("工单不存在: " + id)); + } + + @Override + public List getAllWorkOrders() { + return workOrderRepository.findAll(); + } + + @Override + public List getWorkOrdersByProject(UUID projectId) { + return workOrderRepository.findByProjectId(projectId); + } + + @Override + public List getWorkOrdersByEquipment(UUID equipmentId) { + return workOrderRepository.findByEquipmentId(equipmentId); + } + + @Override + public List getWorkOrdersBySource(WorkOrder.Source source) { + return workOrderRepository.findBySource(source); + } + + @Override + public List getWorkOrdersByType(WorkOrder.Type type) { + return workOrderRepository.findByType(type); + } + + @Override + public List getWorkOrdersByStatus(WorkOrder.Status status) { + return workOrderRepository.findByStatus(status); + } + + @Override + public List getWorkOrdersByAssignedTo(String assignedTo) { + return workOrderRepository.findByAssignedTo(assignedTo); + } + + @Override + public List getOverdueWorkOrders(LocalDate date) { + return workOrderRepository.findOverdueTasks(date); + } + + @Override + @Transactional + public WorkOrder assignWorkOrder(UUID id, String assignedTo, String assignedVendor, LocalDate assignedDate) { + WorkOrder workOrder = getWorkOrderById(id); + + if (workOrder.getStatus() != WorkOrder.Status.PENDING) { + throw new RuntimeException("只能分配待派单状态的工单"); + } + + workOrder.setAssignedTo(assignedTo); + workOrder.setAssignedVendor(assignedVendor); + workOrder.setAssignedDate(assignedDate); + workOrder.setStatus(WorkOrder.Status.ASSIGNED); + + return workOrderRepository.save(workOrder); + } + + @Override + @Transactional + public WorkOrder startWorkOrder(UUID id) { + WorkOrder workOrder = getWorkOrderById(id); + + if (workOrder.getStatus() != WorkOrder.Status.ASSIGNED) { + throw new RuntimeException("只能启动已分配的工单"); + } + + workOrder.setStatus(WorkOrder.Status.IN_PROGRESS); + workOrder.setActualStart(LocalDateTime.now()); + + return workOrderRepository.save(workOrder); + } + + @Override + @Transactional + public WorkOrder completeWorkOrder(UUID id, WorkOrder workOrderData) { + WorkOrder workOrder = getWorkOrderById(id); + + if (workOrder.getStatus() != WorkOrder.Status.IN_PROGRESS) { + throw new RuntimeException("只能完成进行中的工单"); + } + + workOrder.setActualEnd(LocalDateTime.now()); + + // 计算实际工时 + if (workOrder.getActualStart() != null) { + BigDecimal hours = BigDecimal.valueOf( + java.time.Duration.between(workOrder.getActualStart(), workOrder.getActualEnd()).toMinutes() + ).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); + workOrder.setActualHours(hours); + } + + // 更新故障原因和解决方案 + workOrder.setFaultCause(workOrderData.getFaultCause()); + workOrder.setSolution(workOrderData.getSolution()); + workOrder.setResult(workOrderData.getResult()); + + // 更新费用信息 + workOrder.setLaborCost(workOrderData.getLaborCost()); + workOrder.setPartsCost(workOrderData.getPartsCost()); + workOrder.setTotalCost(workOrderData.getTotalCost()); + + // 更新完成信息 + workOrder.setCompletedBy(workOrderData.getCompletedBy()); + workOrder.setCompletedDate(LocalDate.now()); + + workOrder.setStatus(WorkOrder.Status.COMPLETED); + + return workOrderRepository.save(workOrder); + } + + @Override + @Transactional + public WorkOrder verifyWorkOrder(UUID id, String verifiedBy, String remark, Integer rating) { + WorkOrder workOrder = getWorkOrderById(id); + + if (workOrder.getStatus() != WorkOrder.Status.COMPLETED) { + throw new RuntimeException("只能验收已完成的工单"); + } + + workOrder.setVerifiedBy(verifiedBy); + workOrder.setVerifiedDate(LocalDate.now()); + workOrder.setRemark(remark); + workOrder.setRating(rating); + workOrder.setStatus(WorkOrder.Status.VERIFIED); + + return workOrderRepository.save(workOrder); + } + + @Override + @Transactional + public WorkOrder cancelWorkOrder(UUID id) { + WorkOrder workOrder = getWorkOrderById(id); + + if (workOrder.getStatus() == WorkOrder.Status.COMPLETED || + workOrder.getStatus() == WorkOrder.Status.VERIFIED) { + throw new RuntimeException("无法取消已完成的工单"); + } + + workOrder.setStatus(WorkOrder.Status.CANCELLED); + return workOrderRepository.save(workOrder); + } + + @Override + public WorkOrderStatsDTO getWorkOrderStats() { + LocalDate today = LocalDate.now(); + LocalDateTime startOfToday = today.atStartOfDay(); + LocalDateTime endOfToday = today.plusDays(1).atStartOfDay(); + + // 按状态统计 + Map byStatus = new HashMap<>(); + List statusCounts = workOrderRepository.countByStatus(); + long total = 0; + for (Object[] row : statusCounts) { + WorkOrder.Status status = (WorkOrder.Status) row[0]; + Long count = (Long) row[1]; + byStatus.put(status.name(), count); + total += count; + } + + // 按来源统计 + Map bySource = new HashMap<>(); + List sourceCounts = workOrderRepository.countBySource(); + for (Object[] row : sourceCounts) { + WorkOrder.Source source = (WorkOrder.Source) row[0]; + Long count = (Long) row[1]; + bySource.put(source.name(), count); + } + + // 按类型统计 + Map byType = new HashMap<>(); + List typeCounts = workOrderRepository.countByType(); + for (Object[] row : typeCounts) { + WorkOrder.Type type = (WorkOrder.Type) row[0]; + Long count = (Long) row[1]; + byType.put(type.name(), count); + } + + // 按优先级统计 + Map byPriority = new HashMap<>(); + List priorityCounts = workOrderRepository.countByPriority(); + for (Object[] row : priorityCounts) { + WorkOrder.Priority priority = (WorkOrder.Priority) row[0]; + Long count = (Long) row[1]; + byPriority.put(priority.name(), count); + } + + return WorkOrderStatsDTO.builder() + .total(total) + .pending(byStatus.getOrDefault("PENDING", 0L)) + .assigned(byStatus.getOrDefault("ASSIGNED", 0L)) + .inProgress(byStatus.getOrDefault("IN_PROGRESS", 0L)) + .completed(byStatus.getOrDefault("COMPLETED", 0L)) + .verified(byStatus.getOrDefault("VERIFIED", 0L)) + .cancelled(byStatus.getOrDefault("CANCELLED", 0L)) + .completedToday(workOrderRepository.countCompletedToday(today)) + .createdToday(workOrderRepository.countByCreatedAtBetween(startOfToday, endOfToday)) + .overdue(workOrderRepository.countOverdue(today)) + .bySource(bySource) + .byType(byType) + .byPriority(byPriority) + .build(); + } + + @Override + public List getWorkOrderItems(UUID workOrderId) { + return workOrderItemRepository.findByWorkOrderIdOrderBySortOrder(workOrderId); + } + + @Override + @Transactional + public WorkOrder addWorkOrderItem(UUID workOrderId, WorkOrderItem item) { + WorkOrder workOrder = getWorkOrderById(workOrderId); + item.setWorkOrderId(workOrderId); + workOrderItemRepository.save(item); + return workOrder; + } + + @Override + @Transactional + public WorkOrder addWorkOrderItems(UUID workOrderId, List items) { + WorkOrder workOrder = getWorkOrderById(workOrderId); + for (WorkOrderItem item : items) { + item.setWorkOrderId(workOrderId); + workOrderItemRepository.save(item); + } + return workOrder; + } + + /** + * 生成工单编号 WO-YYYYMMDD-XXXX + */ + private String generateWorkNo() { + String datePrefix = "WO-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-"; + String maxWorkNo = workOrderRepository.findMaxWorkNoByPrefix(datePrefix + "%"); + + int sequence = 1; + if (maxWorkNo != null && maxWorkNo.length() >= datePrefix.length()) { + String lastSequence = maxWorkNo.substring(datePrefix.length()); + try { + sequence = Integer.parseInt(lastSequence) + 1; + } catch (NumberFormatException e) { + sequence = 1; + } + } + + return datePrefix + String.format("%04d", sequence); + } +} \ No newline at end of file diff --git a/module-wo/src/main/resources/db/migration/V1.0__create_work_order_tables.sql b/module-wo/src/main/resources/db/migration/V1.0__create_work_order_tables.sql new file mode 100644 index 0000000..28260e1 --- /dev/null +++ b/module-wo/src/main/resources/db/migration/V1.0__create_work_order_tables.sql @@ -0,0 +1,162 @@ +-- 工单主表 +CREATE TABLE ops_work_order ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + work_no VARCHAR(50) UNIQUE NOT NULL, -- 工单编号 WO-YYYYMMDD-XXXX + + -- 工单分类 + source VARCHAR(50) NOT NULL, -- OWNER|MAINTENANCE|INSPECTION|FAULT|REGULATORY|MANUAL + type VARCHAR(50) NOT NULL, -- REPAIR|INSPECTION|SECURITY|CLEANING|PROPERTY|CONSULTATION + + -- 基本信息 + title VARCHAR(200) NOT NULL, + description TEXT, + priority VARCHAR(20) NOT NULL DEFAULT 'MEDIUM', -- LOW|MEDIUM|HIGH|URGENT + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING|ASSIGNED|IN_PROGRESS|COMPLETED|VERIFIED|CANCELLED + + -- 关联信息 + project_id UUID, + equipment_id UUID, + space_id UUID, + plan_id UUID, -- 来源计划ID(维保计划/巡检模板) + trigger_type VARCHAR(50), -- PLAN|INSPECTION|FAULT|MANUAL + + -- 派单信息 + assigned_to VARCHAR(200), + assigned_vendor VARCHAR(200), + assigned_date DATE, + + -- 执行信息 + actual_start TIMESTAMP, + actual_end TIMESTAMP, + actual_hours DECIMAL(6,2), + + -- 问题与解决方案 + fault_cause TEXT, + solution TEXT, + result TEXT, + + -- 费用信息 + labor_cost DECIMAL(12,2), + parts_cost DECIMAL(12,2), + total_cost DECIMAL(12,2), + + -- 完成信息 + completed_by VARCHAR(200), + completed_date DATE, + + -- 验收信息 + verified_by VARCHAR(200), + verified_date DATE, + rating INT CHECK (rating >= 1 AND rating <= 5), + remark TEXT, + + -- 扩展信息 + photos JSONB, + signature TEXT, + + -- 审计字段 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(200) +); + +-- 工单项表(维修备件/巡检项) +CREATE TABLE ops_work_order_item ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + work_order_id UUID NOT NULL REFERENCES ops_work_order(id) ON DELETE CASCADE, + + item_type VARCHAR(50) NOT NULL, -- PART|INSPECTION_ITEM|CHECKPOINT + item_name VARCHAR(200) NOT NULL, + quantity DECIMAL(10,2) DEFAULT 1, + unit VARCHAR(50), + unit_price DECIMAL(12,2), + total_price DECIMAL(12,2), + + -- 巡检项特有 + is_normal BOOLEAN, + observation TEXT, + suggestion TEXT, + + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 维保计划表 +CREATE TABLE ops_maintenance_plan ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + plan_code VARCHAR(50) UNIQUE NOT NULL, + plan_name VARCHAR(200) NOT NULL, + plan_content TEXT, + + project_id UUID, + equipment_id UUID NOT NULL, + plan_type VARCHAR(50) NOT NULL, -- PREVENTIVE|CORRECTIVE + + cycle_days INT, -- 周期(天) + estimated_hours DECIMAL(6,2), + assigned_vendor VARCHAR(200), + + status VARCHAR(20) DEFAULT 'ACTIVE', -- ACTIVE|INACTIVE|SUSPENDED + + last_date DATE, + next_date DATE, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(200) +); + +-- 巡检模板表 +CREATE TABLE ops_inspection_template ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + template_code VARCHAR(50) UNIQUE NOT NULL, + template_name VARCHAR(200) NOT NULL, + description TEXT, + + project_id UUID, + space_id UUID, -- 适用于该空间及子空间 + category VARCHAR(50), -- 巡检分类 + + status VARCHAR(20) DEFAULT 'ACTIVE', + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(200) +); + +-- 巡检标准项表 +CREATE TABLE ops_inspection_item ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + template_id UUID NOT NULL REFERENCES ops_inspection_template(id) ON DELETE CASCADE, + + item_name VARCHAR(200) NOT NULL, + description TEXT, + check_method TEXT, + standard TEXT, + + is_mandatory BOOLEAN DEFAULT TRUE, + is_normal_required BOOLEAN DEFAULT TRUE, -- 是否必须填写正常/异常 + + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 索引 +CREATE INDEX idx_wo_project ON ops_work_order(project_id); +CREATE INDEX idx_wo_equipment ON ops_work_order(equipment_id); +CREATE INDEX idx_wo_space ON ops_work_order(space_id); +CREATE INDEX idx_wo_source ON ops_work_order(source); +CREATE INDEX idx_wo_type ON ops_work_order(type); +CREATE INDEX idx_wo_status ON ops_work_order(status); +CREATE INDEX idx_wo_priority ON ops_work_order(priority); +CREATE INDEX idx_wo_assigned_to ON ops_work_order(assigned_to); +CREATE INDEX idx_wo_created_at ON ops_work_order(created_at); + +CREATE INDEX idx_woi_work_order ON ops_work_order_item(work_order_id); + +CREATE INDEX idx_mp_project ON ops_maintenance_plan(project_id); +CREATE INDEX idx_mp_equipment ON ops_maintenance_plan(equipment_id); +CREATE INDEX idx_mp_next_date ON ops_maintenance_plan(next_date); +CREATE INDEX idx_mp_status ON ops_maintenance_plan(status); + +CREATE INDEX idx_it_template ON ops_inspection_item(template_id); \ No newline at end of file diff --git a/module-wo/src/test/java/com/ether/pms/ops/service/impl/WorkOrderServiceTest.java b/module-wo/src/test/java/com/ether/pms/ops/service/impl/WorkOrderServiceTest.java new file mode 100644 index 0000000..ccfffa8 --- /dev/null +++ b/module-wo/src/test/java/com/ether/pms/ops/service/impl/WorkOrderServiceTest.java @@ -0,0 +1,349 @@ +package com.ether.pms.ops.service.impl; + +import com.ether.pms.ops.entity.WorkOrder; +import com.ether.pms.ops.repository.WorkOrderItemRepository; +import com.ether.pms.ops.repository.WorkOrderRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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.math.BigDecimal; +import java.time.LocalDate; +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.*; + +/** + * 工单服务测试类 (TDD) + * + * 测试原则:RED(红) → GREEN(绿) → REFACTOR(重构) + */ +@ExtendWith(MockitoExtension.class) +@DisplayName("工单服务测试") +class WorkOrderServiceTest { + + @Mock + private WorkOrderRepository workOrderRepository; + + @Mock + private WorkOrderItemRepository workOrderItemRepository; + + @InjectMocks + private WorkOrderServiceImpl workOrderService; + + private WorkOrder testWorkOrder; + private UUID testId; + + @BeforeEach + void setUp() { + testId = UUID.randomUUID(); + testWorkOrder = new WorkOrder(); + testWorkOrder.setId(testId); + testWorkOrder.setWorkNo("WO-20260328-0001"); + testWorkOrder.setTitle("测试工单"); + testWorkOrder.setSource(WorkOrder.Source.OWNER); + testWorkOrder.setType(WorkOrder.Type.REPAIR); + testWorkOrder.setPriority(WorkOrder.Priority.MEDIUM); + testWorkOrder.setStatus(WorkOrder.Status.PENDING); + testWorkOrder.setDescription("测试描述"); + testWorkOrder.setCreatedAt(LocalDateTime.now()); + } + + @Nested + @DisplayName("工单创建测试") + class CreateWorkOrderTests { + + @Test + @DisplayName("创建工单时应自动生成工单编号") + void shouldGenerateWorkNoOnCreate() { + when(workOrderRepository.findMaxWorkNoByPrefix(any())).thenReturn(null); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> { + WorkOrder saved = invocation.getArgument(0); + saved.setId(UUID.randomUUID()); + return saved; + }); + + WorkOrder result = workOrderService.createWorkOrder(testWorkOrder); + + assertNotNull(result.getWorkNo()); + assertTrue(result.getWorkNo().startsWith("WO-")); + assertTrue(result.getWorkNo().contains(LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")))); + verify(workOrderRepository).save(any(WorkOrder.class)); + } + + @Test + @DisplayName("创建工单时应设置默认状态为PENDING") + void shouldSetDefaultStatusPending() { + testWorkOrder.setStatus(null); + when(workOrderRepository.findMaxWorkNoByPrefix(any())).thenReturn(null); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + WorkOrder result = workOrderService.createWorkOrder(testWorkOrder); + + assertEquals(WorkOrder.Status.PENDING, result.getStatus()); + } + + @Test + @DisplayName("创建工单时应设置默认优先级为MEDIUM") + void shouldSetDefaultPriorityMedium() { + testWorkOrder.setPriority(null); + when(workOrderRepository.findMaxWorkNoByPrefix(any())).thenReturn(null); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + WorkOrder result = workOrderService.createWorkOrder(testWorkOrder); + + assertEquals(WorkOrder.Priority.MEDIUM, result.getPriority()); + } + + @Test + @DisplayName("创建工单时应递增序号") + void shouldIncrementSequence() { + when(workOrderRepository.findMaxWorkNoByPrefix(any())).thenReturn("WO-20260405-0015"); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + WorkOrder result = workOrderService.createWorkOrder(testWorkOrder); + + assertEquals("WO-20260405-0016", result.getWorkNo()); + } + } + + @Nested + @DisplayName("工单状态流转测试") + class StateTransitionTests { + + @Test + @DisplayName("派单:PENDING -> ASSIGNED") + void assignWorkOrder_shouldChangeStatusToAssigned() { + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + WorkOrder result = workOrderService.assignWorkOrder(testId, "张三", "维保公司A", LocalDate.now()); + + assertEquals(WorkOrder.Status.ASSIGNED, result.getStatus()); + assertEquals("张三", result.getAssignedTo()); + assertEquals("维保公司A", result.getAssignedVendor()); + assertNotNull(result.getAssignedDate()); + } + + @Test + @DisplayName("派单失败:只有PENDING状态才能派单") + void assignWorkOrder_shouldFailWhenNotPending() { + testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED); + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + + assertThrows(RuntimeException.class, () -> + workOrderService.assignWorkOrder(testId, "张三", "维保公司A", LocalDate.now()) + ); + } + + @Test + @DisplayName("开始:ASSIGNED -> IN_PROGRESS") + void startWorkOrder_shouldChangeStatusToInProgress() { + testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED); + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + WorkOrder result = workOrderService.startWorkOrder(testId); + + assertEquals(WorkOrder.Status.IN_PROGRESS, result.getStatus()); + assertNotNull(result.getActualStart()); + } + + @Test + @DisplayName("开始失败:只有ASSIGNED状态才能开始") + void startWorkOrder_shouldFailWhenNotAssigned() { + testWorkOrder.setStatus(WorkOrder.Status.PENDING); + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + + assertThrows(RuntimeException.class, () -> + workOrderService.startWorkOrder(testId) + ); + } + + @Test + @DisplayName("完成:IN_PROGRESS -> COMPLETED") + void completeWorkOrder_shouldChangeStatusToCompleted() { + testWorkOrder.setStatus(WorkOrder.Status.IN_PROGRESS); + testWorkOrder.setActualStart(LocalDateTime.now().minusHours(2)); + + WorkOrder completeData = new WorkOrder(); + completeData.setFaultCause("设备故障"); + completeData.setSolution("更换零件"); + completeData.setResult("已修复"); + completeData.setLaborCost(BigDecimal.valueOf(200)); + completeData.setPartsCost(BigDecimal.valueOf(500)); + completeData.setTotalCost(BigDecimal.valueOf(700)); + completeData.setCompletedBy("李四"); + + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + WorkOrder result = workOrderService.completeWorkOrder(testId, completeData); + + assertEquals(WorkOrder.Status.COMPLETED, result.getStatus()); + assertEquals("设备故障", result.getFaultCause()); + assertEquals("更换零件", result.getSolution()); + assertEquals("已修复", result.getResult()); + assertEquals(BigDecimal.valueOf(700), result.getTotalCost()); + assertNotNull(result.getActualEnd()); + assertNotNull(result.getCompletedDate()); + } + + @Test + @DisplayName("完成失败:只有IN_PROGRESS状态才能完成") + void completeWorkOrder_shouldFailWhenNotInProgress() { + testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED); + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + + assertThrows(RuntimeException.class, () -> + workOrderService.completeWorkOrder(testId, new WorkOrder()) + ); + } + + @Test + @DisplayName("验收:COMPLETED -> VERIFIED") + void verifyWorkOrder_shouldChangeStatusToVerified() { + testWorkOrder.setStatus(WorkOrder.Status.COMPLETED); + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + WorkOrder result = workOrderService.verifyWorkOrder(testId, "王五", "验收通过", 5); + + assertEquals(WorkOrder.Status.VERIFIED, result.getStatus()); + assertEquals("王五", result.getVerifiedBy()); + assertEquals("验收通过", result.getRemark()); + assertEquals(5, result.getRating()); + assertNotNull(result.getVerifiedDate()); + } + + @Test + @DisplayName("验收失败:只有COMPLETED状态才能验收") + void verifyWorkOrder_shouldFailWhenNotCompleted() { + testWorkOrder.setStatus(WorkOrder.Status.IN_PROGRESS); + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + + assertThrows(RuntimeException.class, () -> + workOrderService.verifyWorkOrder(testId, "王五", "验收通过", 5) + ); + } + + @Test + @DisplayName("验收评分应在1-5范围内") + void verifyWorkOrder_shouldAcceptValidRating() { + testWorkOrder.setStatus(WorkOrder.Status.COMPLETED); + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + WorkOrder result = workOrderService.verifyWorkOrder(testId, "王五", null, 4); + + assertEquals(4, result.getRating()); + } + + @Test + @DisplayName("取消:PENDING/ASSIGNED/IN_PROGRESS -> CANCELLED") + void cancelWorkOrder_shouldChangeStatusToCancelled() { + testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED); + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + WorkOrder result = workOrderService.cancelWorkOrder(testId); + + assertEquals(WorkOrder.Status.CANCELLED, result.getStatus()); + } + + @Test + @DisplayName("取消失败:COMPLETED状态不能取消") + void cancelWorkOrder_shouldFailWhenCompleted() { + testWorkOrder.setStatus(WorkOrder.Status.COMPLETED); + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + + assertThrows(RuntimeException.class, () -> + workOrderService.cancelWorkOrder(testId) + ); + } + + @Test + @DisplayName("取消失败:VERIFIED状态不能取消") + void cancelWorkOrder_shouldFailWhenVerified() { + testWorkOrder.setStatus(WorkOrder.Status.VERIFIED); + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + + assertThrows(RuntimeException.class, () -> + workOrderService.cancelWorkOrder(testId) + ); + } + } + + @Nested + @DisplayName("工单查询测试") + class QueryTests { + + @Test + @DisplayName("根据ID获取工单") + void getWorkOrderById_shouldReturnWorkOrder() { + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + + WorkOrder result = workOrderService.getWorkOrderById(testId); + + assertNotNull(result); + assertEquals(testId, result.getId()); + } + + @Test + @DisplayName("根据ID获取工单失败时应抛出异常") + void getWorkOrderById_shouldThrowExceptionWhenNotFound() { + when(workOrderRepository.findById(testId)).thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> + workOrderService.getWorkOrderById(testId) + ); + } + } + + @Nested + @DisplayName("工单删除测试") + class DeleteTests { + + @Test + @DisplayName("删除工单应调用repository") + void deleteWorkOrder_shouldCallRepository() { + workOrderService.deleteWorkOrder(testId); + + verify(workOrderRepository).deleteById(testId); + } + } + + @Nested + @DisplayName("工单更新测试") + class UpdateTests { + + @Test + @DisplayName("更新工单应保存所有字段") + void updateWorkOrder_shouldUpdateAllFields() { + when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); + when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + WorkOrder updateData = new WorkOrder(); + updateData.setTitle("更新标题"); + updateData.setDescription("更新描述"); + updateData.setPriority(WorkOrder.Priority.HIGH); + updateData.setLaborCost(BigDecimal.valueOf(100)); + + WorkOrder result = workOrderService.updateWorkOrder(testId, updateData); + + assertEquals("更新标题", result.getTitle()); + assertEquals("更新描述", result.getDescription()); + assertEquals(WorkOrder.Priority.HIGH, result.getPriority()); + assertEquals(BigDecimal.valueOf(100), result.getLaborCost()); + } + } +} diff --git a/pms-starter/pom.xml b/pms-starter/pom.xml index 0620dd8..7cf7bd9 100644 --- a/pms-starter/pom.xml +++ b/pms-starter/pom.xml @@ -37,7 +37,7 @@ com.ether - module-ops + module-wo ${project.version} diff --git a/pms-starter/src/main/resources/application.yml b/pms-starter/src/main/resources/application.yml index 4baecc4..00e2a73 100644 --- a/pms-starter/src/main/resources/application.yml +++ b/pms-starter/src/main/resources/application.yml @@ -64,6 +64,6 @@ login: lockout-duration-minutes: 10 jwt: - secret: ether-pms-secret-key-must-be-at-least-256-bits-long-for-hs256 + secret: ${JWT_SECRET:ether-pms-jwt-secret-key-for-development-only-change-in-production-min-256-bits} expiration: 86400000 issuer: ether-pms diff --git a/pom.xml b/pom.xml index 511fe03..3eb338d 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ module-common module-auth module-mdm - module-ops + module-wo module-asset module-finance pms-starter diff --git a/scripts/execute-equipment-ddl.sh b/scripts/execute-equipment-ddl.sh new file mode 100755 index 0000000..e9d2c00 --- /dev/null +++ b/scripts/execute-equipment-ddl.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# 设备模块数据库脚本执行 +# 执行前请确保数据库服务正在运行 + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +DDL_FILE="$PROJECT_DIR/module-mdm/src/main/resources/db/migration/V20260325__create_equipment_tables.sql" +MIGRATION_FILE="$PROJECT_DIR/module-mdm/src/main/resources/db/migration/V20260326__migrate_equipment_data.sql" + +# 数据库连接参数 +DB_HOST="localhost" +DB_PORT="5432" +DB_NAME="ether_pms" +DB_USER="chiguyong" + +echo "==========================================" +echo "设备模块数据库脚本执行" +echo "==========================================" + +# 检查psql是否可用 +if ! command -v psql &> /dev/null; then + echo "错误: psql 未安装或不在PATH中" + echo "请安装PostgreSQL客户端: brew install postgresql" + exit 1 +fi + +# 检查DDL文件是否存在 +if [ ! -f "$DDL_FILE" ]; then + echo "错误: DDL文件不存在: $DDL_FILE" + exit 1 +fi + +# 1. 执行DDL脚本 +echo "" +echo "[1/2] 执行设备表DDL脚本..." +echo "文件: $DDL_FILE" +echo "" + +psql -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -U "$DB_USER" -f "$DDL_FILE" + +if [ $? -eq 0 ]; then + echo "" + echo "✅ DDL脚本执行成功" +else + echo "" + echo "❌ DDL脚本执行失败" + exit 1 +fi + +# 2. 询问是否执行数据迁移 +echo "" +echo "==========================================" +read -p "是否执行数据迁移脚本? (y/N): " CONFIRM_MIGRATION +echo "==========================================" + +if [ "$CONFIRM_MIGRATION" = "y" ] || [ "$CONFIRM_MIGRATION" = "Y" ]; then + if [ ! -f "$MIGRATION_FILE" ]; then + echo "错误: 迁移文件不存在: $MIGRATION_FILE" + exit 1 + fi + + echo "" + echo "[2/2] 执行数据迁移脚本..." + echo "文件: $MIGRATION_FILE" + echo "" + + # 注意:迁移脚本中的函数需要手动调用 + # 这里只执行函数创建,实际迁移需要单独执行 + psql -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -U "$DB_USER" -f "$MIGRATION_FILE" + + if [ $? -eq 0 ]; then + echo "" + echo "✅ 迁移脚本执行成功" + echo "" + echo "注意: 实际数据迁移需要执行以下SQL:" + echo " SELECT migrate_equipment_from_space_node();" + echo "" + echo "如需回滚,执行:" + echo " SELECT rollback_equipment_migration();" + else + echo "" + echo "❌ 迁移脚本执行失败" + exit 1 + fi +else + echo "" + echo "跳过了数据迁移" +fi + +echo "" +echo "==========================================" +echo "脚本执行完成" +echo "==========================================" diff --git a/sql/V1__system_init.sql b/sql/V1__system_init.sql new file mode 100644 index 0000000..9ac9554 --- /dev/null +++ b/sql/V1__system_init.sql @@ -0,0 +1,242 @@ +-- ============================================================ +-- Ether 系统初始化脚本 V1 +-- 创建日期: 2026-03-28 +-- 说明: 初始化认证授权模块的数据库表结构 +-- 包含: 用户、角色、权限、审计日志、系统配置 +-- ============================================================ + +BEGIN; + +-- ============================================================ +-- 第一部分:用户表 (auth_user) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS auth_user ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + salt VARCHAR(50), + real_name VARCHAR(100), + phone VARCHAR(20), + email VARCHAR(100), + avatar VARCHAR(500), + status VARCHAR(20) DEFAULT 'ACTIVE', + last_login_time TIMESTAMP, + last_login_ip VARCHAR(50), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by UUID, + CONSTRAINT auth_user_status_check CHECK (status IN ('ACTIVE', 'DISABLED', 'DELETED')) +); + +COMMENT ON TABLE auth_user IS '系统用户表'; +COMMENT ON COLUMN auth_user.id IS '用户唯一标识'; +COMMENT ON COLUMN auth_user.username IS '用户名(登录账号)'; +COMMENT ON COLUMN auth_user.password IS '加密后的密码'; +COMMENT ON COLUMN auth_user.salt IS '密码盐值'; +COMMENT ON COLUMN auth_user.real_name IS '真实姓名'; +COMMENT ON COLUMN auth_user.phone IS '手机号码'; +COMMENT ON COLUMN auth_user.email IS '电子邮箱'; +COMMENT ON COLUMN auth_user.avatar IS '头像URL'; +COMMENT ON COLUMN auth_user.status IS '状态:ACTIVE-正常 DISABLED-禁用 DELETED-已删除'; +COMMENT ON COLUMN auth_user.last_login_time IS '最后登录时间'; +COMMENT ON COLUMN auth_user.last_login_ip IS '最后登录IP'; +COMMENT ON COLUMN auth_user.created_at IS '创建时间'; +COMMENT ON COLUMN auth_user.updated_at IS '更新时间'; +COMMENT ON COLUMN auth_user.created_by IS '创建人ID'; + +-- 用户索引 +CREATE INDEX IF NOT EXISTS idx_auth_user_username ON auth_user(username); +CREATE INDEX IF NOT EXISTS idx_auth_user_status ON auth_user(status); +CREATE INDEX IF NOT EXISTS idx_auth_user_phone ON auth_user(phone); +CREATE INDEX IF NOT EXISTS idx_auth_user_email ON auth_user(email); + +-- ============================================================ +-- 第二部分:角色表 (auth_role) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS auth_role ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + type VARCHAR(20) NOT NULL DEFAULT 'SYSTEM', + data_scope VARCHAR(20) NOT NULL DEFAULT 'SELF', + project_id UUID, + status VARCHAR(20) NOT NULL DEFAULT 'ENABLED', + sort_order INT DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT auth_role_type_check CHECK (type IN ('SYSTEM', 'PROJECT', 'DEPARTMENT')), + CONSTRAINT auth_role_data_scope_check CHECK (data_scope IN ('ALL', 'PROJECT', 'DEPARTMENT', 'SELF')), + CONSTRAINT auth_role_status_check CHECK (status IN ('ENABLED', 'DISABLED')) +); + +COMMENT ON TABLE auth_role IS '系统角色表'; +COMMENT ON COLUMN auth_role.id IS '角色唯一标识'; +COMMENT ON COLUMN auth_role.code IS '角色编码(唯一)'; +COMMENT ON COLUMN auth_role.name IS '角色名称'; +COMMENT ON COLUMN auth_role.description IS '角色描述'; +COMMENT ON COLUMN auth_role.type IS '角色类型:SYSTEM-系统级 PROJECT-项目级 DEPARTMENT-部门级'; +COMMENT ON COLUMN auth_role.data_scope IS '数据范围:ALL-全部 PROJECT-项目级 DEPARTMENT-部门级 SELF-仅本人'; +COMMENT ON COLUMN auth_role.project_id IS '所属项目ID(项目级角色使用)'; +COMMENT ON COLUMN auth_role.status IS '状态:ENABLED-启用 DISABLED-禁用'; +COMMENT ON COLUMN auth_role.sort_order IS '排序号'; + +-- 角色索引 +CREATE INDEX IF NOT EXISTS idx_auth_role_code ON auth_role(code); +CREATE INDEX IF NOT EXISTS idx_auth_role_type ON auth_role(type); +CREATE INDEX IF NOT EXISTS idx_auth_role_status ON auth_role(status); + +-- ============================================================ +-- 第三部分:权限表 (auth_permission) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS auth_permission ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code VARCHAR(100) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + type VARCHAR(20) NOT NULL DEFAULT 'BUTTON', + resource VARCHAR(255), + method VARCHAR(20), + action VARCHAR(30), + module VARCHAR(50), + description VARCHAR(500), + sort_order INT DEFAULT 0, + parent_code VARCHAR(100) REFERENCES auth_permission(code), + status VARCHAR(20) NOT NULL DEFAULT 'ENABLED', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT auth_permission_type_check CHECK (type IN ('MENU', 'BUTTON', 'API')), + CONSTRAINT auth_permission_action_check CHECK (action IN ('VIEW', 'CREATE', 'EDIT', 'DELETE', 'EXPORT', 'IMPORT', 'APPROVE', 'ASSIGN')) +); + +COMMENT ON TABLE auth_permission IS '系统权限表'; +COMMENT ON COLUMN auth_permission.id IS '权限唯一标识'; +COMMENT ON COLUMN auth_permission.code IS '权限编码(唯一,格式:模块:资源:操作)'; +COMMENT ON COLUMN auth_permission.name IS '权限名称'; +COMMENT ON COLUMN auth_permission.type IS '权限类型:MENU-菜单 BUTTON-按钮 API-接口'; +COMMENT ON COLUMN auth_permission.resource IS '资源路径'; +COMMENT ON COLUMN auth_permission.method IS 'HTTP方法:GET POST PUT DELETE'; +COMMENT ON COLUMN auth_permission.action IS '操作类型:VIEW CREATE EDIT DELETE EXPORT IMPORT APPROVE ASSIGN'; +COMMENT ON COLUMN auth_permission.module IS '所属模块'; +COMMENT ON COLUMN auth_permission.description IS '权限描述'; +COMMENT ON COLUMN auth_permission.sort_order IS '排序号'; +COMMENT ON COLUMN auth_permission.parent_code IS '父权限编码(用于树形结构)'; +COMMENT ON COLUMN auth_permission.status IS '状态:ENABLED-启用 DISABLED-禁用'; + +-- 权限索引 +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_module ON auth_permission(module); +CREATE INDEX IF NOT EXISTS idx_auth_permission_status ON auth_permission(status); +CREATE INDEX IF NOT EXISTS idx_auth_permission_parent ON auth_permission(parent_code); + +-- ============================================================ +-- 第四部分:用户角色关联表 (auth_user_role) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS auth_user_role ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE, + role_id UUID NOT NULL REFERENCES auth_role(id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT auth_user_role_unique UNIQUE (user_id, role_id) +); + +COMMENT ON TABLE auth_user_role IS '用户角色关联表'; +COMMENT ON COLUMN auth_user_role.user_id IS '用户ID'; +COMMENT ON COLUMN auth_user_role.role_id IS '角色ID'; + +-- 用户角色关联索引 +CREATE INDEX IF NOT EXISTS idx_auth_user_role_user ON auth_user_role(user_id); +CREATE INDEX IF NOT EXISTS idx_auth_user_role_role ON auth_user_role(role_id); + +-- ============================================================ +-- 第五部分:角色权限关联表 (auth_role_permission) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS auth_role_permission ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + role_id UUID NOT NULL REFERENCES auth_role(id) ON DELETE CASCADE, + permission_id UUID NOT NULL REFERENCES auth_permission(id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT auth_role_permission_unique UNIQUE (role_id, permission_id) +); + +COMMENT ON TABLE auth_role_permission IS '角色权限关联表'; +COMMENT ON COLUMN auth_role_permission.role_id IS '角色ID'; +COMMENT ON COLUMN auth_role_permission.permission_id IS '权限ID'; + +-- 角色权限关联索引 +CREATE INDEX IF NOT EXISTS idx_auth_role_permission_role ON auth_role_permission(role_id); +CREATE INDEX IF NOT EXISTS idx_auth_role_permission_permission ON auth_role_permission(permission_id); + +-- ============================================================ +-- 第六部分:审计日志表 (auth_audit_log) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS auth_audit_log ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES auth_user(id), + username VARCHAR(50), + module VARCHAR(50), + action VARCHAR(30), + operation VARCHAR(200), + resource VARCHAR(255), + method VARCHAR(20), + ip_address VARCHAR(50), + location VARCHAR(200), + user_agent TEXT, + request_body TEXT, + response_status INT, + error_message TEXT, + execution_time_ms INT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE auth_audit_log IS '审计日志表'; +COMMENT ON COLUMN auth_audit_log.user_id IS '操作用户ID'; +COMMENT ON COLUMN auth_audit_log.username IS '操作用户名'; +COMMENT ON COLUMN auth_audit_log.module IS '功能模块'; +COMMENT ON COLUMN auth_audit_log.action IS '操作类型'; +COMMENT ON COLUMN auth_audit_log.operation IS '操作描述'; +COMMENT ON COLUMN auth_audit_log.resource IS '资源路径'; +COMMENT ON COLUMN auth_audit_log.method IS 'HTTP方法'; +COMMENT ON COLUMN auth_audit_log.ip_address IS 'IP地址'; +COMMENT ON COLUMN auth_audit_log.location IS '地理位置'; +COMMENT ON COLUMN auth_audit_log.user_agent IS '用户代理'; +COMMENT ON COLUMN auth_audit_log.request_body IS '请求体'; +COMMENT ON COLUMN auth_audit_log.response_status IS '响应状态码'; +COMMENT ON COLUMN auth_audit_log.error_message IS '错误信息'; +COMMENT ON COLUMN auth_audit_log.execution_time_ms IS '执行时长(毫秒)'; +COMMENT ON COLUMN auth_audit_log.created_at IS '操作时间'; + +-- 审计日志索引 +CREATE INDEX IF NOT EXISTS idx_auth_audit_log_user ON auth_audit_log(user_id); +CREATE INDEX IF NOT EXISTS idx_auth_audit_log_module ON auth_audit_log(module); +CREATE INDEX IF NOT EXISTS idx_auth_audit_log_action ON auth_audit_log(action); +CREATE INDEX IF NOT EXISTS idx_auth_audit_log_created ON auth_audit_log(created_at); + +-- ============================================================ +-- 第七部分:系统配置表 (auth_sys_config) +-- ============================================================ + +CREATE TABLE IF NOT EXISTS auth_sys_config ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + config_key VARCHAR(128) NOT NULL UNIQUE, + config_value TEXT, + description VARCHAR(256), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE auth_sys_config IS '系统配置表'; +COMMENT ON COLUMN auth_sys_config.config_key IS '配置键(唯一)'; +COMMENT ON COLUMN auth_sys_config.config_value IS '配置值'; +COMMENT ON COLUMN auth_sys_config.description IS '配置描述'; + +-- 系统配置索引 +CREATE INDEX IF NOT EXISTS idx_auth_sys_config_key ON auth_sys_config(config_key); + +COMMIT; diff --git a/sql/V2.1__project_enhancements.sql b/sql/V2.1__project_enhancements.sql deleted file mode 100644 index 7a0917f..0000000 --- a/sql/V2.1__project_enhancements.sql +++ /dev/null @@ -1,99 +0,0 @@ --- 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/V2.2__system_config.sql b/sql/V2.2__system_config.sql deleted file mode 100644 index bc6da67..0000000 --- a/sql/V2.2__system_config.sql +++ /dev/null @@ -1,22 +0,0 @@ --- ============================================ --- System Config Table --- 系统配置表 --- ============================================ - -CREATE TABLE IF NOT EXISTS sys_config ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - config_key VARCHAR(128) NOT NULL UNIQUE, - config_value TEXT, - description VARCHAR(256), - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_sys_config_key ON sys_config(config_key); - --- 初始化系统配置 -INSERT INTO sys_config (config_key, config_value, description) VALUES - ('property_company_name', '物业管理有限公司', '物业企业名称'), - ('property_company_address', '', '物业企业地址'), - ('property_company_phone', '', '物业企业电话') -ON CONFLICT (config_key) DO NOTHING; diff --git a/sql/V2.3__space_node_and_project_config.sql b/sql/V2.3__space_node_and_project_config.sql new file mode 100644 index 0000000..961e75b --- /dev/null +++ b/sql/V2.3__space_node_and_project_config.sql @@ -0,0 +1,83 @@ +-- ============================================ +-- V2.3__space_node_equipment_fields.sql +-- 同步 mdm_space_node 表与 SpaceNode 实体 +-- ============================================ + +-- 添加基础字段(如果有缺失) +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS parent_id UUID; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS project_id UUID; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS tree_path VARCHAR(1000); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS tree_path_name VARCHAR(1000); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS full_name VARCHAR(200); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS short_name VARCHAR(100); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS node_category VARCHAR(50); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS usage_type VARCHAR(50); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS building_area DECIMAL(12,2); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS usable_area DECIMAL(12,2); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS shared_area DECIMAL(12,2); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS land_area DECIMAL(12,2); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS floor_number INTEGER; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS level INTEGER; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS province VARCHAR(50); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS city VARCHAR(50); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS district VARCHAR(50); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS street VARCHAR(200); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS longitude DECIMAL(10,6); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS latitude DECIMAL(10,6); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS altitude DECIMAL(10,2); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS delivery_status VARCHAR(50); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS decoration_status VARCHAR(50); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN DEFAULT FALSE; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS created_by UUID; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS updated_by UUID; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS attributes TEXT; + +-- 设备扩展字段 +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS is_equipment BOOLEAN DEFAULT FALSE; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS design_life_years INTEGER; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS rated_power DECIMAL(10,2); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS rated_voltage DECIMAL(10,2); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS rated_current DECIMAL(10,2); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS maintenance_contract_no VARCHAR(100); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS maintenance_contract_start DATE; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS maintenance_contract_end DATE; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS maintenance_vendor VARCHAR(200); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS maintenance_vendor_phone VARCHAR(20); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS maintenance_vendor_contact VARCHAR(100); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS special_equipment_type VARCHAR(50); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS special_equipment_cert VARCHAR(100); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS inspection_cycle INTEGER; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS next_inspection_date DATE; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS last_inspection_date DATE; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS last_inspection_result VARCHAR(20); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS common_spare_parts TEXT; +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS energy_consumption_standard DECIMAL(12,2); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS installation_environment VARCHAR(50); +ALTER TABLE mdm_space_node ADD COLUMN IF NOT EXISTS protection_level VARCHAR(20); + +-- 添加索引 +CREATE INDEX IF NOT EXISTS idx_space_node_project ON mdm_space_node(project_id); +CREATE INDEX IF NOT EXISTS idx_space_node_parent ON mdm_space_node(parent_id); +CREATE INDEX IF NOT EXISTS idx_space_node_type ON mdm_space_node(node_type); + +-- ============================================ +-- ProjectConfig 表(如不存在则创建) +-- ============================================ +CREATE TABLE IF NOT EXISTS mdm_project_config ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID NOT NULL, + 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); diff --git a/sql/V2.4__fix_rated_voltage_type.sql b/sql/V2.4__fix_rated_voltage_type.sql new file mode 100644 index 0000000..59407c8 --- /dev/null +++ b/sql/V2.4__fix_rated_voltage_type.sql @@ -0,0 +1,2 @@ +-- Fix rated_voltage column type (numeric -> varchar) +ALTER TABLE mdm_space_node ALTER COLUMN rated_voltage TYPE varchar(20); diff --git a/sql/V2__permission_init.sql b/sql/V2__permission_init.sql new file mode 100644 index 0000000..a2f0f6a --- /dev/null +++ b/sql/V2__permission_init.sql @@ -0,0 +1,254 @@ +-- ============================================================ +-- Ether 权限角色初始化脚本 V2 +-- 创建日期: 2026-03-28 +-- 说明: 初始化系统角色、权限和默认用户 +-- API路径已修正为 /api/auth/* 格式 +-- ============================================================ + +BEGIN; + +-- ============================================================ +-- 第一部分:清理旧数据 +-- ============================================================ + +DELETE FROM auth_role_permission; +DELETE FROM auth_user_role; +DELETE FROM auth_permission; +DELETE FROM auth_role; +DELETE FROM auth_user; + +-- ============================================================ +-- 第二部分:初始化默认用户 +-- ============================================================ + +-- 管理员用户 +-- 密码: Admin@123 (BCrypt加密) +INSERT INTO auth_user (username, password, real_name, status) +VALUES ('admin', '$2a$10$cgqoZgzRAM1kvtp59z/UYOMQW8/Cd0eE5MBCgwN5bgvAJ9kgQxZXO', '系统管理员', 'ACTIVE'); + +-- 业主用户(用于测试) +-- 密码: Admin@123 (BCrypt加密) +INSERT INTO auth_user (username, password, real_name, status) +VALUES ('owner1', '$2a$10$cgqoZgzRAM1kvtp59z/UYOMQW8/Cd0eE5MBCgwN5bgvAJ9kgQxZXO', '测试业主', 'ACTIVE'); + +-- 员工用户(用于测试) +-- 密码: Admin@123 (BCrypt加密) +INSERT INTO auth_user (username, password, real_name, status) +VALUES ('employee1', '$2a$10$cgqoZgzRAM1kvtp59z/UYOMQW8/Cd0eE5MBCgwN5bgvAJ9kgQxZXO', '测试员工', 'ACTIVE'); + +-- ============================================================ +-- 第三部分:初始化角色 +-- ============================================================ + +-- 系统管理员角色 +INSERT INTO auth_role (code, name, description, type, data_scope, status, sort_order) +VALUES ('SYS_ADMIN', '系统管理员', '系统级管理,负责系统配置、用户管理、角色权限管理', 'SYSTEM', 'ALL', 'ENABLED', 1); + +-- 项目管理员角色 +INSERT INTO auth_role (code, name, description, type, data_scope, status, sort_order) +VALUES ('PROJECT_ADMIN', '项目管理员', '项目级管理,负责本项目内的所有操作', 'PROJECT', 'PROJECT', 'ENABLED', 10); + +-- 工程主管角色 +INSERT INTO auth_role (code, name, description, type, data_scope, status, sort_order) +VALUES ('ENGINEERING_LEAD', '工程主管', '工程部管理,负责设备维护和工单调度', 'DEPARTMENT', 'DEPARTMENT', 'ENABLED', 11); + +-- 安保主管角色 +INSERT INTO auth_role (code, name, description, type, data_scope, status, sort_order) +VALUES ('SECURITY_LEAD', '安保主管', '安保部管理,负责安保巡检和访客管理', 'DEPARTMENT', 'DEPARTMENT', 'ENABLED', 12); + +-- 客服人员角色 +INSERT INTO auth_role (code, name, description, type, data_scope, status, sort_order) +VALUES ('CS_STAFF', '客服人员', '业主服务、访客核验', 'PROJECT', 'PROJECT', 'ENABLED', 20); + +-- 保洁人员角色 +INSERT INTO auth_role (code, name, description, type, data_scope, status, sort_order) +VALUES ('CLEANING_STAFF', '保洁人员', '保洁执行、品质检查', 'SYSTEM', 'SELF', 'ENABLED', 23); + +-- 业主角色 +INSERT INTO auth_role (code, name, description, type, data_scope, status, sort_order) +VALUES ('OWNER', '业主', '业主用户,可查看个人账单和报修', 'SYSTEM', 'SELF', 'ENABLED', 30); + +-- ============================================================ +-- 第四部分:初始化权限 +-- ============================================================ + +-- 4.1 系统管理模块权限 +-- 用户管理权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('system:user:menu', '用户管理菜单', 'MENU', '/system/users', 'GET', 'VIEW', 'system', '用户管理菜单', 101), + ('system:user:list', '用户列表', 'API', '/api/auth/users', 'GET', 'VIEW', 'system', '查看用户列表', 102), + ('system:user:create', '创建用户', 'API', '/api/auth/users', 'POST', 'CREATE', 'system', '创建新用户', 103), + ('system:user:update', '更新用户', 'API', '/api/auth/users/*', 'PUT', 'EDIT', 'system', '更新用户信息', 104), + ('system:user:delete', '删除用户', 'API', '/api/auth/users/*', 'DELETE', 'DELETE', 'system', '删除用户', 105), + ('system:user:assignRole', '分配角色', 'API', '/api/auth/users/*/roles', 'POST', 'ASSIGN', 'system', '为用户分配角色', 106), + ('system:user:resetPassword', '重置密码', 'API', '/api/auth/users/*/password', 'PUT', 'EDIT', 'system', '重置用户密码', 107); + +-- 角色管理权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('system:role:menu', '角色管理菜单', 'MENU', '/system/roles', 'GET', 'VIEW', 'system', '角色管理菜单', 201), + ('system:role:list', '角色列表', 'API', '/api/auth/roles', 'GET', 'VIEW', 'system', '查看角色列表', 202), + ('system:role:create', '创建角色', 'API', '/api/auth/roles', 'POST', 'CREATE', 'system', '创建新角色', 203), + ('system:role:update', '更新角色', 'API', '/api/auth/roles/*', 'PUT', 'EDIT', 'system', '更新角色信息', 204), + ('system:role:delete', '删除角色', 'API', '/api/auth/roles/*', 'DELETE', 'DELETE', 'system', '删除角色', 205), + ('system:role:assignPermission', '分配权限', 'API', '/api/auth/roles/*/permissions', 'POST', 'ASSIGN', 'system', '为角色分配权限', 206); + +-- 权限管理权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('system:permission:menu', '权限管理菜单', 'MENU', '/system/permissions', 'GET', 'VIEW', 'system', '权限管理菜单', 301), + ('system:permission:list', '权限列表', 'API', '/api/auth/permissions', 'GET', 'VIEW', 'system', '查看权限列表', 302), + ('system:permission:create', '创建权限', 'API', '/api/auth/permissions', 'POST', 'CREATE', 'system', '创建新权限', 303), + ('system:permission:update', '更新权限', 'API', '/api/auth/permissions/*', 'PUT', 'EDIT', 'system', '更新权限信息', 304), + ('system:permission:delete', '删除权限', 'API', '/api/auth/permissions/*', 'DELETE', 'DELETE', 'system', '删除权限', 305); + +-- 系统设置权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('system:config:menu', '系统设置菜单', 'MENU', '/system/settings', 'GET', 'VIEW', 'system', '系统设置菜单', 401), + ('system:config:view', '查看系统设置', 'API', '/api/auth/config', 'GET', 'VIEW', 'system', '查看系统设置', 402), + ('system:config:update', '更新系统设置', 'API', '/api/auth/config/*', 'PUT', 'EDIT', 'system', '更新系统设置', 403); + +-- 审计日志权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('system:audit:menu', '审计日志菜单', 'MENU', '/system/audit', 'GET', 'VIEW', 'system', '审计日志菜单', 501), + ('system:audit:list', '审计日志列表', 'API', '/api/auth/audit', 'GET', 'VIEW', 'system', '查看审计日志', 502), + ('system:audit:export', '导出审计日志', 'API', '/api/auth/audit/export', 'GET', 'EXPORT', 'system', '导出审计日志', 503); + +-- 4.2 项目管理模块权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('project:menu', '项目管理菜单', 'MENU', '/project/list', 'GET', 'VIEW', 'project', '项目管理菜单', 601), + ('project:list', '项目列表', 'API', '/api/project/projects', 'GET', 'VIEW', 'project', '查看项目列表', 602), + ('project:create', '创建项目', 'API', '/api/project/projects', 'POST', 'CREATE', 'project', '创建新项目', 603), + ('project:update', '更新项目', 'API', '/api/project/projects/*', 'PUT', 'EDIT', 'project', '更新项目信息', 604), + ('project:delete', '删除项目', 'API', '/api/project/projects/*', 'DELETE', 'DELETE', 'project', '删除项目', 605); + +-- 4.3 空间管理模块权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('space:menu', '空间管理菜单', 'MENU', '/project/*/space', 'GET', 'VIEW', 'space', '空间管理菜单', 701), + ('space:list', '空间列表', 'API', '/api/project/spaces', 'GET', 'VIEW', 'space', '查看空间列表', 702), + ('space:create', '创建空间', 'API', '/api/project/spaces', 'POST', 'CREATE', 'space', '创建新空间', 703), + ('space:update', '更新空间', 'API', '/api/project/spaces/*', 'PUT', 'EDIT', 'space', '更新空间信息', 704), + ('space:delete', '删除空间', 'API', '/api/project/spaces/*', 'DELETE', 'DELETE', 'space', '删除空间', 705); + +-- 4.4 设备管理模块权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('equipment:menu', '设备管理菜单', 'MENU', '/equipment/list', 'GET', 'VIEW', 'equipment', '设备管理菜单', 801), + ('equipment:list', '设备列表', 'API', '/api/mdm/equipments', 'GET', 'VIEW', 'equipment', '查看设备列表', 802), + ('equipment:create', '创建设备', 'API', '/api/mdm/equipments', 'POST', 'CREATE', 'equipment', '创建新设备', 803), + ('equipment:update', '更新设备', 'API', '/api/mdm/equipments/*', 'PUT', 'EDIT', 'equipment', '更新设备信息', 804), + ('equipment:delete', '删除设备', 'API', '/api/mdm/equipments/*', 'DELETE', 'DELETE', 'equipment', '删除设备', 805); + +-- 4.5 能耗管理模块权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('energy:menu', '能耗管理菜单', 'MENU', '/energy/meters', 'GET', 'VIEW', 'energy', '能耗管理菜单', 901), + ('energy:meter:list', '计量点列表', 'API', '/api/mdm/meters', 'GET', 'VIEW', 'energy', '查看计量点列表', 902), + ('energy:consumption:record', '能耗录入', 'API', '/api/mdm/consumptions', 'POST', 'CREATE', 'energy', '录入能耗数据', 903), + ('energy:consumption:view', '查看能耗', 'API', '/api/mdm/consumptions', 'GET', 'VIEW', 'energy', '查看能耗数据', 904), + ('energy:statistics:view', '能耗统计', 'API', '/api/mdm/consumptions/statistics', 'GET', 'VIEW', 'energy', '查看能耗统计', 905); + +-- 4.6 工单运维模块权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('ops:workOrder:menu', '工单管理菜单', 'MENU', '/operation/work-orders', 'GET', 'VIEW', 'ops', '工单管理菜单', 1001), + ('ops:workOrder:list', '工单列表', 'API', '/api/ops/work-orders', 'GET', 'VIEW', 'ops', '查看工单列表', 1002), + ('ops:workOrder:create', '创建工单', 'API', '/api/ops/work-orders', 'POST', 'CREATE', 'ops', '创建新工单', 1003), + ('ops:workOrder:update', '更新工单', 'API', '/api/ops/work-orders/*', 'PUT', 'EDIT', 'ops', '更新工单信息', 1004), + ('ops:workOrder:assign', '分配工单', 'API', '/api/ops/work-orders/*/assign', 'POST', 'ASSIGN', 'ops', '分配工单给处理人', 1005), + ('ops:workOrder:close', '关闭工单', 'API', '/api/ops/work-orders/*/close', 'POST', 'EDIT', 'ops', '关闭工单', 1006); + +-- 4.7 巡检模块权限 +INSERT INTO auth_permission (code, name, type, resource, method, action, module, description, sort_order) +VALUES + ('ops:inspection:menu', '巡检管理菜单', 'MENU', '/operation/inspections', 'GET', 'VIEW', 'ops', '巡检管理菜单', 1101), + ('ops:inspection:list', '巡检列表', 'API', '/api/ops/inspections', 'GET', 'VIEW', 'ops', '查看巡检列表', 1102), + ('ops:inspection:create', '创建巡检', 'API', '/api/ops/inspections', 'POST', 'CREATE', 'ops', '创建新巡检', 1103), + ('ops:inspection:execute', '执行巡检', 'API', '/api/ops/inspections/*/execute', 'POST', 'EDIT', 'ops', '执行巡检任务', 1104); + +-- ============================================================ +-- 第五部分:角色权限关联 +-- ============================================================ + +-- 系统管理员拥有所有权限 +INSERT INTO auth_role_permission (role_id, permission_id) +SELECT r.id, p.id +FROM auth_role r, auth_permission p +WHERE r.code = 'SYS_ADMIN'; + +-- 项目管理员拥有项目、空间、设备、工单、巡检相关权限 +INSERT INTO auth_role_permission (role_id, permission_id) +SELECT r.id, p.id +FROM auth_role r, auth_permission p +WHERE r.code = 'PROJECT_ADMIN' + AND p.module IN ('project', 'space', 'equipment', 'energy', 'ops'); + +-- 工程主管拥有设备、工单相关权限 +INSERT INTO auth_role_permission (role_id, permission_id) +SELECT r.id, p.id +FROM auth_role r, auth_permission p +WHERE r.code = 'ENGINEERING_LEAD' + AND p.module IN ('equipment', 'ops'); + +-- 安保主管拥有巡检、工单相关权限 +INSERT INTO auth_role_permission (role_id, permission_id) +SELECT r.id, p.id +FROM auth_role r, auth_permission p +WHERE r.code = 'SECURITY_LEAD' + AND p.module IN ('ops'); + +-- 客服人员拥有工单相关权限 +INSERT INTO auth_role_permission (role_id, permission_id) +SELECT r.id, p.id +FROM auth_role r, auth_permission p +WHERE r.code = 'CS_STAFF' + AND p.code LIKE 'ops:workOrder:%'; + +-- 保洁人员拥有巡检相关权限 +INSERT INTO auth_role_permission (role_id, permission_id) +SELECT r.id, p.id +FROM auth_role r, auth_permission p +WHERE r.code = 'CLEANING_STAFF' + AND p.code LIKE 'ops:inspection:%'; + +-- ============================================================ +-- 第六部分:用户角色关联 +-- ============================================================ + +-- admin用户 -> 系统管理员 +INSERT INTO auth_user_role (user_id, role_id) +SELECT u.id, r.id +FROM auth_user u, auth_role r +WHERE u.username = 'admin' AND r.code = 'SYS_ADMIN'; + +-- owner1用户 -> 业主 +INSERT INTO auth_user_role (user_id, role_id) +SELECT u.id, r.id +FROM auth_user u, auth_role r +WHERE u.username = 'owner1' AND r.code = 'OWNER'; + +-- employee1用户 -> 客服人员 +INSERT INTO auth_user_role (user_id, role_id) +SELECT u.id, r.id +FROM auth_user u, auth_role r +WHERE u.username = 'employee1' AND r.code = 'CS_STAFF'; + +-- ============================================================ +-- 第七部分:系统配置初始化 +-- ============================================================ + +INSERT INTO auth_sys_config (config_key, config_value, description) +VALUES + ('property_company_name', '示例物业有限公司', '物业企业名称'), + ('system.version', '1.0.0', '系统版本'), + ('audit.retention_days', '30', '审计日志保留天数') +ON CONFLICT (config_key) DO UPDATE SET + config_value = EXCLUDED.config_value, + description = EXCLUDED.description; + +COMMIT; diff --git a/sql/V3__user_extension.sql b/sql/V3__user_extension.sql new file mode 100644 index 0000000..cf28475 --- /dev/null +++ b/sql/V3__user_extension.sql @@ -0,0 +1,204 @@ +-- ============================================================ +-- V3__user_extension.sql +-- 用户权限体系扩展脚本 +-- 创建部门表、企业员工扩展表、项目员工扩展表、业主扩展表、房屋表、业主房屋绑定表 +-- 同时扩展 auth_user 和 auth_role 表 +-- ============================================================ + +BEGIN; + +-- ============================================================ +-- 1. 扩展 auth_user 表 - 添加用户类型和部门字段 +-- ============================================================ + +ALTER TABLE auth_user ADD COLUMN IF NOT EXISTS user_type VARCHAR(20) DEFAULT 'ENTERPRISE'; +ALTER TABLE auth_user ADD COLUMN IF NOT EXISTS dept_id UUID; + +-- ============================================================ +-- 2. 创建部门表 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS dept ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + parent_id UUID REFERENCES dept(id) ON DELETE SET NULL, + dept_name VARCHAR(100) NOT NULL, + dept_code VARCHAR(50) UNIQUE, + leader_id UUID REFERENCES auth_user(id) ON DELETE SET NULL, + sort_order INT DEFAULT 0, + status VARCHAR(20) DEFAULT 'ACTIVE', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_dept_parent ON dept(parent_id); + +-- ============================================================ +-- 3. 创建企业员工扩展表 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS enterprise_user ( + user_id UUID PRIMARY KEY REFERENCES auth_user(id) ON DELETE CASCADE, + employee_no VARCHAR(50) UNIQUE, + dept_id UUID REFERENCES dept(id) ON DELETE SET NULL, + position VARCHAR(100), + entry_date DATE, + user_category VARCHAR(20) DEFAULT 'ENTERPRISE', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_enterprise_user_dept ON enterprise_user(dept_id); + +-- ============================================================ +-- 4. 创建项目员工扩展表 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS project_staff ( + user_id UUID PRIMARY KEY REFERENCES auth_user(id) ON DELETE CASCADE, + project_id UUID NOT NULL, + staff_type VARCHAR(20) DEFAULT 'GENERAL', + shift_type VARCHAR(20) DEFAULT 'DAY', + leader_id UUID REFERENCES auth_user(id) ON DELETE SET NULL, + assignment_status VARCHAR(20) DEFAULT 'ASSIGNED', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_project_staff_project ON project_staff(project_id); +CREATE INDEX IF NOT EXISTS idx_project_staff_user ON project_staff(user_id); + +-- ============================================================ +-- 5. 创建业主扩展表 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS resident ( + user_id UUID PRIMARY KEY REFERENCES auth_user(id) ON DELETE CASCADE, + id_card VARCHAR(18), + resident_type VARCHAR(20) DEFAULT 'OWNER', + verification_status VARCHAR(20) DEFAULT 'UNVERIFIED', + verified_at TIMESTAMP, + verified_by UUID REFERENCES auth_user(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_resident_type ON resident(resident_type); + +-- ============================================================ +-- 6. 创建房屋表 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS space ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + project_id UUID NOT NULL, + building VARCHAR(50) NOT NULL, + unit VARCHAR(20) NOT NULL, + room_no VARCHAR(20) NOT NULL, + space_type VARCHAR(20) DEFAULT 'RESIDENTIAL', + floor INT, + unit_area DECIMAL(10, 2), + status VARCHAR(20) DEFAULT 'ACTIVE', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(project_id, building, unit, room_no) +); + +CREATE INDEX IF NOT EXISTS idx_space_project ON space(project_id); + +-- ============================================================ +-- 7. 创建业主房屋绑定表 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS resident_space ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE, + space_id UUID NOT NULL REFERENCES space(id) ON DELETE CASCADE, + relation_type VARCHAR(20) DEFAULT 'OWNER', + binding_status VARCHAR(20) DEFAULT 'ACTIVE', + start_date DATE, + end_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_resident_space_user ON resident_space(user_id); +CREATE INDEX IF NOT EXISTS idx_resident_space_space ON resident_space(space_id); + +-- ============================================================ +-- 8. 扩展 auth_user_role 表 - 支持项目级角色 +-- ============================================================ + +ALTER TABLE auth_user_role ADD COLUMN IF NOT EXISTS project_id UUID; +ALTER TABLE auth_user_role ADD COLUMN IF NOT EXISTS scope VARCHAR(20) DEFAULT 'ENTERPRISE'; + +CREATE INDEX IF NOT EXISTS idx_user_role_scope ON auth_user_role(project_id, scope); + +-- ============================================================ +-- 9. 更新 auth_role 表 - 增强角色类型 +-- ============================================================ + +ALTER TABLE auth_role ADD COLUMN IF NOT EXISTS scope VARCHAR(20) DEFAULT 'ENTERPRISE'; +ALTER TABLE auth_role ADD COLUMN IF NOT EXISTS project_id UUID; + +CREATE INDEX IF NOT EXISTS idx_role_scope ON auth_role(scope); + +-- ============================================================ +-- 10. 更新现有数据 +-- ============================================================ + +UPDATE auth_user SET user_type = 'ENTERPRISE' WHERE user_type IS NULL; +UPDATE auth_role SET type = 'SYSTEM', scope = 'ENTERPRISE' WHERE type IS NULL; + +COMMIT; + +-- ============================================================ +-- 添加表和列注释(单独执行) +-- ============================================================ + +COMMENT ON TABLE dept IS '部门表'; +COMMENT ON COLUMN dept.parent_id IS '上级部门ID'; +COMMENT ON COLUMN dept.dept_name IS '部门名称'; +COMMENT ON COLUMN dept.dept_code IS '部门编码'; +COMMENT ON COLUMN dept.leader_id IS '部门负责人ID'; + +COMMENT ON TABLE enterprise_user IS '企业员工扩展表'; +COMMENT ON COLUMN enterprise_user.employee_no IS '员工工号'; +COMMENT ON COLUMN enterprise_user.dept_id IS '所属部门'; +COMMENT ON COLUMN enterprise_user.position IS '职位'; +COMMENT ON COLUMN enterprise_user.entry_date IS '入职日期'; +COMMENT ON COLUMN enterprise_user.user_category IS '员工类别: ENTERPRISE-职能员工, MANAGEMENT-管理岗'; + +COMMENT ON TABLE project_staff IS '项目员工扩展表'; +COMMENT ON COLUMN project_staff.project_id IS '所属项目'; +COMMENT ON COLUMN project_staff.staff_type IS '员工类型: SECURITY-保安, CLEANING-保洁, GARDEN-绿化, MAINTENANCE-维修, CUSTOMER_SERVICE-客服, GENERAL-普通'; +COMMENT ON COLUMN project_staff.shift_type IS '班次类型: DAY-白班, NIGHT-夜班, ROTATION-轮班'; +COMMENT ON COLUMN project_staff.leader_id IS '班组长ID'; +COMMENT ON COLUMN project_staff.assignment_status IS '在岗状态: ASSIGNED-在岗, ON_LEAVE-休假, TRANSFERRED-调岗'; + +COMMENT ON TABLE resident IS '业主住户扩展表'; +COMMENT ON COLUMN resident.id_card IS '身份证号'; +COMMENT ON COLUMN resident.resident_type IS '住户类型: OWNER-业主, FAMILY-家庭成员, TENANT-租户'; +COMMENT ON COLUMN resident.verification_status IS '认证状态: UNVERIFIED-未认证, PENDING-待审核, VERIFIED-已认证, REJECTED-已拒绝'; +COMMENT ON COLUMN resident.verified_by IS '认证人ID'; + +COMMENT ON TABLE space IS '房屋表'; +COMMENT ON COLUMN space.project_id IS '所属项目'; +COMMENT ON COLUMN space.building IS '楼栋'; +COMMENT ON COLUMN space.unit IS '单元'; +COMMENT ON COLUMN space.room_no IS '房号'; +COMMENT ON COLUMN space.space_type IS '房屋类型: RESIDENTIAL-住宅, COMMERCIAL-商办'; +COMMENT ON COLUMN space.floor IS '楼层'; +COMMENT ON COLUMN space.unit_area IS '建筑面积'; + +COMMENT ON TABLE resident_space IS '业主房屋绑定表'; +COMMENT ON COLUMN resident_space.user_id IS '用户ID'; +COMMENT ON COLUMN resident_space.space_id IS '房屋ID'; +COMMENT ON COLUMN resident_space.relation_type IS '关系类型: OWNER-业主, FAMILY-家属, TENANT-租户'; +COMMENT ON COLUMN resident_space.binding_status IS '绑定状态: PENDING-待生效, ACTIVE-生效, EXPIRED-已过期, CANCELLED-已解绑'; +COMMENT ON COLUMN resident_space.start_date IS '绑定开始日期'; +COMMENT ON COLUMN resident_space.end_date IS '绑定结束日期'; + +COMMENT ON COLUMN auth_user.user_type IS '用户类型: ENTERPRISE-企业员工, PROJECT_STAFF-项目员工, RESIDENT-业主, CUSTOMER-客户'; +COMMENT ON COLUMN auth_user.dept_id IS '所属部门ID'; +COMMENT ON COLUMN auth_user_role.project_id IS '项目级角色的所属项目'; +COMMENT ON COLUMN auth_user_role.scope IS '角色生效范围: ENTERPRISE-企业级, PROJECT-项目级, RESIDENT-住户级'; +COMMENT ON COLUMN auth_role.scope IS '角色生效范围: ENTERPRISE-企业级, PROJECT-项目级'; +COMMENT ON COLUMN auth_role.project_id IS '项目级角色的所属项目'; diff --git a/sql/V4__dept_extension.sql b/sql/V4__dept_extension.sql new file mode 100644 index 0000000..7d3f5bb --- /dev/null +++ b/sql/V4__dept_extension.sql @@ -0,0 +1,34 @@ +-- ============================================================ +-- V4__dept_extension.sql +-- 部门扩展脚本 +-- 添加部门类型和默认角色字段 +-- 添加项目员工的部门关联 +-- ============================================================ + +BEGIN; + +-- ============================================================ +-- 1. 扩展 dept 表 - 添加部门类型和默认角色 +-- ============================================================ + +ALTER TABLE dept ADD COLUMN IF NOT EXISTS dept_type VARCHAR(20) DEFAULT 'ADMIN'; +ALTER TABLE dept ADD COLUMN IF NOT EXISTS default_role_code VARCHAR(50); + +COMMENT ON COLUMN dept.dept_type IS '部门类型:ADMIN-行政管理、ENGINEERING-工程部、SECURITY-安保部、CS-客服部、CLEANING-保洁部'; +COMMENT ON COLUMN dept.default_role_code IS '部门默认角色编码'; + +-- ============================================================ +-- 2. 扩展 project_staff 表 - 添加部门关联 +-- ============================================================ + +ALTER TABLE project_staff ADD COLUMN IF NOT EXISTS dept_id UUID REFERENCES dept(id) ON DELETE SET NULL; + +CREATE INDEX IF NOT EXISTS idx_project_staff_dept ON project_staff(dept_id); + +-- ============================================================ +-- 3. 更新 enterprise_user 表的部门关联为非空(企业员工必须有部门) +-- ============================================================ + +-- 注意:已有数据的dept_id可能为NULL,需要先处理 + +COMMIT; diff --git a/sql/V4__project_staff_multi_role.sql b/sql/V4__project_staff_multi_role.sql new file mode 100644 index 0000000..e222d54 --- /dev/null +++ b/sql/V4__project_staff_multi_role.sql @@ -0,0 +1,81 @@ +-- ============================================================ +-- V4__project_staff_multi_role.sql +-- 项目员工多角色支持 +-- 修改 project_staff 表支持多角色,添加 project_staff_role 关联表 +-- ============================================================ + +BEGIN; + +-- ============================================================ +-- 1. 修改 project_staff 表 - 添加 UUID 主键,添加唯一约束 +-- ============================================================ + +-- 如果存在旧主键约束则删除 +ALTER TABLE project_staff DROP CONSTRAINT IF EXISTS project_staff_pkey; + +-- 添加 UUID 主键列 +ALTER TABLE project_staff ADD COLUMN IF NOT EXISTS id UUID PRIMARY KEY DEFAULT gen_random_uuid(); + +-- 添加 user_id + project_id 唯一约束(如果尚未存在) +ALTER TABLE project_staff ADD CONSTRAINT uk_project_staff_user_project UNIQUE (user_id, project_id); + +-- ============================================================ +-- 2. 创建 project_staff_role 关联表 +-- ============================================================ + +CREATE TABLE IF NOT EXISTS project_staff_role ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + staff_id UUID NOT NULL, + role_id UUID NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uk_project_staff_role_staff_role UNIQUE (staff_id, role_id) +); + +CREATE INDEX IF NOT EXISTS idx_project_staff_role_staff ON project_staff_role(staff_id); +CREATE INDEX IF NOT EXISTS idx_project_staff_role_role ON project_staff_role(role_id); + +-- ============================================================ +-- 3. 添加外键约束 +-- ============================================================ + +ALTER TABLE project_staff_role ADD CONSTRAINT fk_project_staff_role_staff + FOREIGN KEY (staff_id) REFERENCES project_staff(id) ON DELETE CASCADE; +ALTER TABLE project_staff_role ADD CONSTRAINT fk_project_staff_role_role + FOREIGN KEY (role_id) REFERENCES auth_role(id); + +-- ============================================================ +-- 4. 迁移现有数据(如果有 role_code 字段) +-- ============================================================ + +-- 检查 role_code 字段是否存在,如果存在则迁移 +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'project_staff' AND column_name = 'role_code') THEN + -- 迁移数据:基于 role_code 关联 auth_role + INSERT INTO project_staff_role (staff_id, role_id, created_at) + SELECT ps.id, r.id, COALESCE(ps.created_at, CURRENT_TIMESTAMP) + FROM project_staff ps + INNER JOIN auth_role r ON r.code = ps.role_code + WHERE ps.role_code IS NOT NULL + ON CONFLICT (staff_id, role_id) DO NOTHING; + END IF; +END $$; + +-- ============================================================ +-- 5. 添加注释 +-- ============================================================ + +COMMENT ON TABLE project_staff_role IS '项目员工角色关联表'; +COMMENT ON COLUMN project_staff_role.staff_id IS '项目员工ID'; +COMMENT ON COLUMN project_staff_role.role_id IS '角色ID'; + +COMMIT; + +-- ============================================================ +-- 回滚脚本(如果需要回滚) +-- ============================================================ + +-- DROP TABLE IF EXISTS project_staff_role; +-- ALTER TABLE project_staff DROP COLUMN IF EXISTS id; +-- ALTER TABLE project_staff ADD CONSTRAINT project_staff_pkey PRIMARY KEY (user_id); +-- ALTER TABLE project_staff DROP CONSTRAINT IF EXISTS uk_project_staff_user_project; diff --git a/sql/V5__init_depts.sql b/sql/V5__init_depts.sql new file mode 100644 index 0000000..a81ce16 --- /dev/null +++ b/sql/V5__init_depts.sql @@ -0,0 +1,69 @@ +-- ============================================================ +-- V5__init_depts.sql +-- 初始化部门数据 +-- 创建物业公司的默认部门结构 +-- ============================================================ + +BEGIN; + +-- ============================================================ +-- 1. 创建顶级部门(物业公司总部) +-- ============================================================ + +INSERT INTO dept (id, dept_name, dept_code, dept_type, default_role_code, parent_id, sort_order, status) +VALUES + ('00000000-0000-0000-0000-000000000001', '物业公司总部', 'HQ', 'ADMIN', NULL, NULL, 0, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000002', '行政管理部', 'ADMIN', 'ADMIN', 'SYS_ADMIN', '00000000-0000-0000-0000-000000000001', 1, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000003', '财务部', 'FINANCE', 'ADMIN', 'SYS_ADMIN', '00000000-0000-0000-0000-000000000001', 2, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000004', '人力资源部', 'HR', 'ADMIN', 'SYS_ADMIN', '00000000-0000-0000-0000-000000000001', 3, 'ACTIVE'); + +-- ============================================================ +-- 2. 创建业务部门 +-- ============================================================ + +INSERT INTO dept (id, dept_name, dept_code, dept_type, default_role_code, parent_id, sort_order, status) +VALUES + ('00000000-0000-0000-0000-000000000010', '工程部', 'ENGINEERING', 'ENGINEERING', 'ENGINEERING_LEAD', '00000000-0000-0000-0000-000000000001', 10, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000011', '安保部', 'SECURITY', 'SECURITY', 'SECURITY_LEAD', '00000000-0000-0000-0000-000000000001', 11, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000012', '客服部', 'CS', 'CS', 'CS_STAFF', '00000000-0000-0000-0000-000000000001', 12, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000013', '保洁部', 'CLEANING', 'CLEANING', 'CLEANING_STAFF', '00000000-0000-0000-0000-000000000001', 13, 'ACTIVE'); + +-- ============================================================ +-- 3. 创建工程部下级班组 +-- ============================================================ + +INSERT INTO dept (id, dept_name, dept_code, dept_type, default_role_code, parent_id, sort_order, status) +VALUES + ('00000000-0000-0000-0000-000000000020', '维修班组', 'ENGINEERING_REPAIR', 'ENGINEERING', 'ENGINEERING_LEAD', '00000000-0000-0000-0000-000000000010', 1, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000021', '电梯班组', 'ENGINEERING_ELEVATOR', 'ENGINEERING', 'ENGINEERING_LEAD', '00000000-0000-0000-0000-000000000010', 2, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000022', '强弱电班组', 'ENGINEERING_ELECTRICAL', 'ENGINEERING', 'ENGINEERING_LEAD', '00000000-0000-0000-0000-000000000010', 3, 'ACTIVE'); + +-- ============================================================ +-- 4. 创建安保部下级班组 +-- ============================================================ + +INSERT INTO dept (id, dept_name, dept_code, dept_type, default_role_code, parent_id, sort_order, status) +VALUES + ('00000000-0000-0000-0000-000000000030', '门禁班组', 'SECURITY_ACCESS', 'SECURITY', 'SECURITY_LEAD', '00000000-0000-0000-0000-000000000011', 1, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000031', '巡逻班组', 'SECURITY_PATROL', 'SECURITY', 'SECURITY_LEAD', '00000000-0000-0000-0000-000000000011', 2, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000032', '监控班组', 'SECURITY_MONITOR', 'SECURITY', 'SECURITY_LEAD', '00000000-0000-0000-0000-000000000011', 3, 'ACTIVE'); + +-- ============================================================ +-- 5. 创建客服部下级 +-- ============================================================ + +INSERT INTO dept (id, dept_name, dept_code, dept_type, default_role_code, parent_id, sort_order, status) +VALUES + ('00000000-0000-0000-0000-000000000040', '前台接待', 'CS_RECEPTION', 'CS', 'CS_STAFF', '00000000-0000-0000-0000-000000000012', 1, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000041', '业主服务', 'CS_OWNER', 'CS', 'CS_STAFF', '00000000-0000-0000-0000-000000000012', 2, 'ACTIVE'); + +-- ============================================================ +-- 6. 创建保洁部下级 +-- ============================================================ + +INSERT INTO dept (id, dept_name, dept_code, dept_type, default_role_code, parent_id, sort_order, status) +VALUES + ('00000000-0000-0000-0000-000000000050', '日常保洁组', 'CLEANING_DAILY', 'CLEANING', 'CLEANING_STAFF', '00000000-0000-0000-0000-000000000013', 1, 'ACTIVE'), + ('00000000-0000-0000-0000-000000000051', '专项保洁组', 'CLEANING_SPECIAL', 'CLEANING', 'CLEANING_STAFF', '00000000-0000-0000-0000-000000000013', 2, 'ACTIVE'); + +COMMIT; diff --git a/sql/fix_space_node_project_code.sql b/sql/fix_space_node_project_code.sql new file mode 100644 index 0000000..efdae91 --- /dev/null +++ b/sql/fix_space_node_project_code.sql @@ -0,0 +1,8 @@ +-- 修改 mdm_space_node 表的 project_code 列类型为 UUID +ALTER TABLE mdm_space_node ALTER COLUMN project_code TYPE uuid USING project_code::uuid; + +-- 修改 mdm_space_node 表的 parent_id 列类型为 UUID (如果还不是) +ALTER TABLE mdm_space_node ALTER COLUMN parent_id TYPE uuid USING parent_id::uuid; + +-- 修改 mdm_space_node 表的 id 列类型为 UUID (如果需要) +-- ALTER TABLE mdm_space_node ALTER COLUMN id TYPE uuid USING id::uuid; diff --git a/sql/init.sql b/sql/init.sql index 17ec85d..82d31ec 100644 --- a/sql/init.sql +++ b/sql/init.sql @@ -139,10 +139,10 @@ CREATE INDEX IF NOT EXISTS idx_mdm_space_node_parent ON mdm_space_node(parent_co -- ============================================ -- Insert default admin user --- Password: Admin123! (BCrypt encrypted) +-- Password: Admin@123 (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$2JRCyrbZANZdGD4sgplVjuIOPvK1P/Be1/4iwXwkUqpbEDo2AHcuC', '系统管理员', 'ACTIVE') ON CONFLICT (username) DO NOTHING; -- Insert default roles