refactor: 重构模块结构,将设备/运维相关代码移至module-asset和module-wo

- 将EquipmentController移至module-asset
- 将EquipmentHealthController移至module-asset
- 将OwnershipEntityController移至module-asset
- 将维保工单相关移至module-wo
- 删除module-ops空模块
- 更新API路径为/api/asset/*和/api/ops/*
This commit is contained in:
chiguyong 2026-04-06 15:41:11 +08:00
parent b24e0818f0
commit ee3cf66c90
195 changed files with 13746 additions and 1618 deletions

View File

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

View File

@ -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<Equipment> createEquipment(@RequestBody Equipment equipment) {
return ApiResponse.success(equipmentService.createEquipment(equipment));
}
@GetMapping("/{id}")
public ApiResponse<Equipment> getEquipment(@PathVariable UUID id) {
return ApiResponse.success(equipmentService.getEquipmentById(id));
}
@PutMapping("/{id}")
public ApiResponse<Equipment> updateEquipment(@PathVariable UUID id, @RequestBody Equipment equipment) {
return ApiResponse.success(equipmentService.updateEquipment(id, equipment));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteEquipment(@PathVariable UUID id) {
equipmentService.deleteEquipment(id);
return ApiResponse.success(null);
}
@PostMapping("/batch-delete")
public ApiResponse<Void> deleteEquipmentBatch(@RequestBody List<UUID> ids) {
equipmentService.deleteEquipmentBatch(ids);
return ApiResponse.success(null);
}
@PostMapping("/import")
public ApiResponse<Map<String, Object>> importEquipment(@RequestParam("file") MultipartFile file, @RequestParam UUID projectId) {
return ApiResponse.success(equipmentService.importFromExcel(file, projectId));
}
@GetMapping("/export")
public ResponseEntity<byte[]> 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<List<Equipment>> getEquipmentsByProject(@PathVariable UUID projectId) {
return ApiResponse.success(equipmentService.getEquipmentsByProject(projectId));
}
@GetMapping("/by-space/{spaceNodeId}")
public ApiResponse<List<Equipment>> getEquipmentsBySpace(@PathVariable UUID spaceNodeId) {
return ApiResponse.success(equipmentService.getEquipmentsBySpaceNode(spaceNodeId));
}
@GetMapping("/by-type")
public ApiResponse<List<Equipment>> getEquipmentsByType(@RequestParam EquipmentType type) {
return ApiResponse.success(equipmentService.getEquipmentsByType(type));
}
@GetMapping("/by-ownership")
public ApiResponse<List<Equipment>> getEquipmentsByOwnership(@RequestParam OwnershipType ownership) {
return ApiResponse.success(equipmentService.getEquipmentsByOwnership(ownership));
}
// ==================== 设备统计 ====================
@GetMapping("/stats/by-type/{projectId}")
public ApiResponse<Map<String, Long>> getStatsByType(@PathVariable UUID projectId) {
return ApiResponse.success(equipmentService.getEquipmentStatsByType(projectId));
}
@GetMapping("/stats/by-ownership/{projectId}")
public ApiResponse<Map<String, Long>> getStatsByOwnership(@PathVariable UUID projectId) {
return ApiResponse.success(equipmentService.getEquipmentStatsByOwnership(projectId));
}
@GetMapping("/stats/count/{projectId}")
public ApiResponse<Long> countByProject(@PathVariable UUID projectId) {
return ApiResponse.success(equipmentService.countByProject(projectId));
}
// ==================== 电梯扩展 ====================
@GetMapping("/{id}/elevator")
public ApiResponse<EquipmentElevator> getElevator(@PathVariable UUID id) {
return ApiResponse.success(
elevatorService.getByEquipmentId(id).orElse(null)
);
}
@PutMapping("/{id}/elevator")
public ApiResponse<EquipmentElevator> updateElevator(@PathVariable UUID id, @RequestBody EquipmentElevator elevator) {
elevator.setEquipmentId(id);
return ApiResponse.success(elevatorService.saveOrUpdate(elevator));
}
// ==================== 暖通扩展 ====================
@GetMapping("/{id}/hvac")
public ApiResponse<EquipmentHvac> getHvac(@PathVariable UUID id) {
return ApiResponse.success(
hvacService.getByEquipmentId(id).orElse(null)
);
}
@PutMapping("/{id}/hvac")
public ApiResponse<EquipmentHvac> updateHvac(@PathVariable UUID id, @RequestBody EquipmentHvac hvac) {
hvac.setEquipmentId(id);
return ApiResponse.success(hvacService.saveOrUpdate(hvac));
}
// ==================== 能源计量扩展 ====================
@GetMapping("/{id}/energy")
public ApiResponse<EquipmentEnergy> getEnergy(@PathVariable UUID id) {
return ApiResponse.success(
energyService.getByEquipmentId(id).orElse(null)
);
}
@PutMapping("/{id}/energy")
public ApiResponse<EquipmentEnergy> updateEnergy(@PathVariable UUID id, @RequestBody EquipmentEnergy energy) {
energy.setEquipmentId(id);
return ApiResponse.success(energyService.saveOrUpdate(energy));
}
// ==================== 消防扩展 ====================
@GetMapping("/{id}/fire")
public ApiResponse<EquipmentFire> getFire(@PathVariable UUID id) {
return ApiResponse.success(
fireService.getByEquipmentId(id).orElse(null)
);
}
@PutMapping("/{id}/fire")
public ApiResponse<EquipmentFire> updateFire(@PathVariable UUID id, @RequestBody EquipmentFire fire) {
fire.setEquipmentId(id);
return ApiResponse.success(fireService.saveOrUpdate(fire));
}
}

View File

@ -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<EquipmentHealthScore> getEquipmentHealth(@PathVariable UUID equipmentId) {
return ApiResponse.success(equipmentHealthService.getLatestHealthScore(equipmentId));
}
/**
* 获取设备健康度历史
*/
@GetMapping("/equipment-health/{equipmentId}/history")
@GetMapping("/{equipmentId}/history")
public ApiResponse<List<EquipmentHealthScore>> getHealthHistory(@PathVariable UUID equipmentId) {
return ApiResponse.success(equipmentHealthService.getHealthHistory(equipmentId));
}
/**
* 计算设备健康度
*/
@PostMapping("/equipment-health/calculate")
@PostMapping("/calculate")
public ApiResponse<EquipmentHealthScore> calculateHealthScore(@RequestBody CalculateHealthRequest request) {
return ApiResponse.success(equipmentHealthService.calculateHealthScore(request.getEquipmentId()));
}
/**
* 记录故障
*/
@PostMapping("/equipment-failure-history")
@PostMapping("/failure-history")
public ApiResponse<EquipmentFailureHistory> recordFailure(@Valid @RequestBody EquipmentFailureHistory failure) {
return ApiResponse.success(equipmentHealthService.recordFailure(failure));
}
/**
* 获取设备故障历史
*/
@GetMapping("/equipment-failure-history/{equipmentId}")
@GetMapping("/failure-history/{equipmentId}")
public ApiResponse<List<EquipmentFailureHistory>> getFailureHistory(@PathVariable UUID equipmentId) {
return ApiResponse.success(equipmentHealthService.getFailureHistory(equipmentId));
}
/**
* 获取设备MTBF平均故障间隔时间
*/
@GetMapping("/equipment-mtbf/{equipmentId}")
@GetMapping("/mtbf/{equipmentId}")
public ApiResponse<MTBFResponse> 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<MTTRResponse> getEquipmentMTTR(
@PathVariable UUID equipmentId,
@RequestParam(required = false, defaultValue = "30") Integer days) {

View File

@ -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<OwnershipEntity> create(@RequestBody OwnershipEntity entity) {
return ApiResponse.success(ownershipEntityRepository.save(entity));
}
@GetMapping("/{id}")
public ApiResponse<OwnershipEntity> getById(@PathVariable UUID id) {
return ApiResponse.success(
ownershipEntityRepository.findByIdAndIsDeletedFalse(id).orElse(null)
);
}
@PutMapping("/{id}")
public ApiResponse<OwnershipEntity> 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<Void> 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<List<OwnershipEntity>> getByType(@RequestParam OwnershipEntity.EntityType type) {
return ApiResponse.success(ownershipEntityRepository.findByEntityTypeAndIsDeletedFalse(type));
}
@GetMapping
public ApiResponse<List<OwnershipEntity>> getAll() {
return ApiResponse.success(ownershipEntityRepository.findByIsDeletedFalse());
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<SparePartInfo> commonSpareParts;
private BigDecimal energyConsumptionStandard;
private String installationEnvironment;
private String protectionLevel;
@Data
public static class SparePartInfo {
private String name;
private String model;
private Integer quantity;
}
}

View File

@ -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<String, Object> attributes;
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "photos", columnDefinition = "jsonb")
private List<EquipmentPhoto> photos;
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "documents", columnDefinition = "jsonb")
private List<EquipmentDocument> 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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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)

View File

@ -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();
}
}

View File

@ -1,4 +1,4 @@
package com.ether.pms.mdm.entity;
package com.ether.pms.asset.entity;
import jakarta.persistence.*;
import lombok.Data;

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<EquipmentElevator, UUID> {
Optional<EquipmentElevator> findByEquipmentId(UUID equipmentId);
void deleteByEquipmentId(UUID equipmentId);
}

View File

@ -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<EquipmentEnergy, UUID> {
Optional<EquipmentEnergy> findByEquipmentId(UUID equipmentId);
void deleteByEquipmentId(UUID equipmentId);
}

View File

@ -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;

View File

@ -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<EquipmentFire, UUID> {
Optional<EquipmentFire> findByEquipmentId(UUID equipmentId);
void deleteByEquipmentId(UUID equipmentId);
}

View File

@ -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;

View File

@ -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<EquipmentHvac, UUID> {
Optional<EquipmentHvac> findByEquipmentId(UUID equipmentId);
void deleteByEquipmentId(UUID equipmentId);
}

View File

@ -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<Equipment, UUID> {
List<Equipment> findByProjectIdAndIsDeletedFalse(UUID projectId);
List<Equipment> findByProjectIdAndStatusAndIsDeletedFalse(UUID projectId, EquipmentStatus status);
List<Equipment> findBySpaceNodeIdAndIsDeletedFalse(UUID spaceNodeId);
List<Equipment> findByEquipmentTypeAndIsDeletedFalse(EquipmentType equipmentType);
List<Equipment> findByOwnershipTypeAndIsDeletedFalse(OwnershipType ownershipType);
Optional<Equipment> findByEquipmentCode(String equipmentCode);
Optional<Equipment> findByIdAndIsDeletedFalse(UUID id);
boolean existsByEquipmentCode(String equipmentCode);
@Query("SELECT e FROM Equipment e WHERE e.projectId = :projectId AND e.isDeleted = false")
List<Equipment> findAllByProject(@Param("projectId") UUID projectId);
@Query("SELECT e FROM Equipment e WHERE e.spaceNodeId = :spaceNodeId AND e.isDeleted = false")
List<Equipment> 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<Object[]> 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<Object[]> countByOwnership(@Param("projectId") UUID projectId);
}

View File

@ -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<OwnershipEntity, UUID> {
List<OwnershipEntity> findByEntityTypeAndIsDeletedFalse(OwnershipEntity.EntityType entityType);
Optional<OwnershipEntity> findByEntityCode(String entityCode);
Optional<OwnershipEntity> findByIdAndIsDeletedFalse(UUID id);
boolean existsByEntityCode(String entityCode);
List<OwnershipEntity> findByIsDeletedFalse();
}

View File

@ -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<EquipmentElevator> getByEquipmentId(UUID equipmentId);
void deleteByEquipmentId(UUID equipmentId);
}

View File

@ -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<EquipmentEnergy> getByEquipmentId(UUID equipmentId);
void deleteByEquipmentId(UUID equipmentId);
}

View File

@ -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<EquipmentFire> getByEquipmentId(UUID equipmentId);
void deleteByEquipmentId(UUID equipmentId);
}

View File

@ -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;

View File

@ -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<EquipmentHvac> getByEquipmentId(UUID equipmentId);
void deleteByEquipmentId(UUID equipmentId);
}

View File

@ -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<Equipment> getEquipmentsByProject(UUID projectId);
List<Equipment> getEquipmentsBySpaceNode(UUID spaceNodeId);
List<Equipment> getEquipmentsByType(EquipmentType equipmentType);
List<Equipment> getEquipmentsByOwnership(OwnershipType ownershipType);
Map<String, Long> getEquipmentStatsByType(UUID projectId);
Map<String, Long> getEquipmentStatsByOwnership(UUID projectId);
long countByProject(UUID projectId);
void deleteEquipmentBatch(List<UUID> ids);
Map<String, Object> importFromExcel(MultipartFile file, UUID projectId);
byte[] exportToExcel(UUID projectId);
}

View File

@ -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<EquipmentElevator> existing = elevatorRepository.findByEquipmentId(elevator.getEquipmentId());
if (existing.isPresent()) {
elevator.setId(existing.get().getId());
}
return elevatorRepository.save(elevator);
}
@Override
public Optional<EquipmentElevator> getByEquipmentId(UUID equipmentId) {
return elevatorRepository.findByEquipmentId(equipmentId);
}
@Override
@Transactional
public void deleteByEquipmentId(UUID equipmentId) {
elevatorRepository.deleteByEquipmentId(equipmentId);
}
}

View File

@ -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<EquipmentEnergy> existing = energyRepository.findByEquipmentId(energy.getEquipmentId());
if (existing.isPresent()) {
energy.setId(existing.get().getId());
}
return energyRepository.save(energy);
}
@Override
public Optional<EquipmentEnergy> getByEquipmentId(UUID equipmentId) {
return energyRepository.findByEquipmentId(equipmentId);
}
@Override
@Transactional
public void deleteByEquipmentId(UUID equipmentId) {
energyRepository.deleteByEquipmentId(equipmentId);
}
}

View File

@ -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<EquipmentFire> existing = fireRepository.findByEquipmentId(fire.getEquipmentId());
if (existing.isPresent()) {
fire.setId(existing.get().getId());
}
return fireRepository.save(fire);
}
@Override
public Optional<EquipmentFire> getByEquipmentId(UUID equipmentId) {
return fireRepository.findByEquipmentId(equipmentId);
}
@Override
@Transactional
public void deleteByEquipmentId(UUID equipmentId) {
fireRepository.deleteByEquipmentId(equipmentId);
}
}

View File

@ -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<MaintenanceTask> 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());
}

View File

@ -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<EquipmentHvac> existing = hvacRepository.findByEquipmentId(hvac.getEquipmentId());
if (existing.isPresent()) {
hvac.setId(existing.get().getId());
}
return hvacRepository.save(hvac);
}
@Override
public Optional<EquipmentHvac> getByEquipmentId(UUID equipmentId) {
return hvacRepository.findByEquipmentId(equipmentId);
}
@Override
@Transactional
public void deleteByEquipmentId(UUID equipmentId) {
hvacRepository.deleteByEquipmentId(equipmentId);
}
}

View File

@ -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<Equipment> getEquipmentsByProject(UUID projectId) {
return equipmentRepository.findByProjectIdAndIsDeletedFalse(projectId);
}
@Override
public List<Equipment> getEquipmentsBySpaceNode(UUID spaceNodeId) {
return equipmentRepository.findBySpaceNodeIdAndIsDeletedFalse(spaceNodeId);
}
@Override
public List<Equipment> getEquipmentsByType(EquipmentType equipmentType) {
return equipmentRepository.findByEquipmentTypeAndIsDeletedFalse(equipmentType);
}
@Override
public List<Equipment> getEquipmentsByOwnership(OwnershipType ownershipType) {
return equipmentRepository.findByOwnershipTypeAndIsDeletedFalse(ownershipType);
}
@Override
public Map<String, Long> getEquipmentStatsByType(UUID projectId) {
List<Object[]> results = equipmentRepository.countByType(projectId);
Map<String, Long> 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<String, Long> getEquipmentStatsByOwnership(UUID projectId) {
List<Object[]> results = equipmentRepository.countByOwnership(projectId);
Map<String, Long> 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<UUID> ids) {
for (UUID id : ids) {
try {
deleteEquipment(id);
} catch (Exception e) {
throw new BusinessException(ErrorCode.BAD_REQUEST, "删除设备失败: " + id);
}
}
}
@Override
@Transactional
public Map<String, Object> importFromExcel(MultipartFile file, UUID projectId) {
List<String> successList = new ArrayList<>();
List<String> 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<String, Object> 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<Equipment> 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;
}
}

View File

@ -0,0 +1,7 @@
package com.ether.pms.asset;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestApplication {
}

View File

@ -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<Equipment> 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<Equipment> found = equipmentRepository.findByIdAndIsDeletedFalse(testEquipment.getId());
assertTrue(found.isEmpty());
}
@Test
void findByIdAndIsDeletedFalse_shouldReturnNonDeletedEquipment() {
entityManager.persist(testEquipment);
entityManager.flush();
Optional<Equipment> 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<Equipment> 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<Equipment> 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<Equipment> 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<Equipment> 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<Equipment> 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<Equipment> 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<Equipment> 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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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;
/**
* 部门管理控制器
*
* <p>提供部门相关的RESTful API接口包括部门树查询创建部门获取部门成员等功能</p>
*
* @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<List<DeptVO>> getDeptTree() {
List<Dept> depts = deptService.getDeptTree();
List<DeptVO> tree = buildDeptTree(depts, null);
return ApiResponse.success(tree);
}
/**
* 获取所有启用的部门列表
*/
@GetMapping
public ApiResponse<List<Dept>> getAllDepts() {
return ApiResponse.success(deptService.getActiveDepts());
}
/**
* 根据ID获取部门
*/
@GetMapping("/{id}")
public ApiResponse<Dept> 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<Dept> 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<Dept> 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<Void> deleteDept(@PathVariable UUID id) {
deptService.deleteDept(id);
return ApiResponse.success();
}
/**
* 获取部门成员
*/
@GetMapping("/{deptId}/members")
public ApiResponse<List<UserVO>> getDeptMembers(@PathVariable UUID deptId) {
List<User> members = deptService.getDeptEmployees(deptId);
return ApiResponse.success(members.stream().map(UserVO::fromEntity).collect(Collectors.toList()));
}
/**
* 根据部门类型查询部门
*/
@GetMapping("/by-type/{deptType}")
public ApiResponse<List<Dept>> getByType(@PathVariable String deptType) {
return ApiResponse.success(deptService.getByType(deptType));
}
private List<DeptVO> buildDeptTree(List<Dept> 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());
}
}

View File

@ -10,43 +10,116 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
/**
* 权限管理REST接口控制器
*
* <p>提供权限Permission相关的HTTP API接口包括权限的增删改查类型筛选菜单权限获取等功能
* 所有接口均遵循RESTful设计规范返回统一的ApiResponse格式</p>
*
* <p>主要接口</p>
* <ul>
* <li>GET /api/auth/permissions - 查询所有权限</li>
* <li>GET /api/auth/permissions/{id} - 根据ID查询权限</li>
* <li>GET /api/auth/permissions/type/{type} - 根据类型查询权限</li>
* <li>GET /api/auth/permissions/menus - 查询所有菜单权限</li>
* <li>POST /api/auth/permissions - 创建权限</li>
* <li>PUT /api/auth/permissions/{id} - 更新权限</li>
* <li>DELETE /api/auth/permissions/{id} - 删除权限</li>
* </ul>
*
* @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<ApiResponse<List<Permission>>> findAll() {
return ResponseEntity.ok(ApiResponse.success(permissionService.findAll()));
}
/**
* 根据权限ID查询权限
*
* @param id 权限ID
* @return 包含权限信息的响应
*/
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Permission>> findById(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(permissionService.findById(id)));
}
/**
* 根据权限类型查询权限列表
*
* <p>按权限类型筛选如MENUBUTTONAPI等</p>
*
* @param type 权限类型
* @return 包含该类型权限列表的响应
*/
@GetMapping("/type/{type}")
public ResponseEntity<ApiResponse<List<Permission>>> findByType(@PathVariable String type) {
return ResponseEntity.ok(ApiResponse.success(permissionService.findByType(type)));
}
/**
* 查询所有菜单权限
*
* <p>获取type为MENU的所有权限通常用于前端菜单渲染</p>
*
* @return 包含菜单权限列表的响应
*/
@GetMapping("/menus")
public ResponseEntity<ApiResponse<List<Permission>>> findMenus() {
return ResponseEntity.ok(ApiResponse.success(permissionService.findMenuPermissions()));
}
/**
* 创建新权限
*
* <p>创建一个新的权限</p>
*
* @param permission 要创建的权限信息请求体
* @return 包含创建后权限信息的响应
*/
@PostMapping
public ResponseEntity<ApiResponse<Permission>> create(@RequestBody Permission permission) {
return ResponseEntity.ok(ApiResponse.success(permissionService.create(permission)));
}
/**
* 更新权限信息
*
* <p>更新指定权限的信息</p>
*
* @param id 要更新的权限ID
* @param permission 包含新数据的权限信息请求体
* @return 包含更新后权限信息的响应
*/
@PutMapping("/{id}")
public ResponseEntity<ApiResponse<Permission>> update(@PathVariable UUID id, @RequestBody Permission permission) {
return ResponseEntity.ok(ApiResponse.success(permissionService.update(id, permission)));
}
/**
* 删除权限
*
* <p>删除指定的权限</p>
*
* @param id 要删除的权限ID
* @return 空响应表示操作成功
*/
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) {
permissionService.delete(id);

View File

@ -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;
/**
* 项目成员管理控制器
*
* <p>提供项目成员相关的RESTful API接口用于管理项目成员列表添加成员移除成员等功能</p>
*
* <p>所有接口路径前缀为/api/auth/projects/{projectId}/members</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@RestController
@RequestMapping("/api/auth/projects")
@RequiredArgsConstructor
public class ProjectMemberController {
/** 用户管理服务 */
private final UserManagementService userManagementService;
/**
* 查询项目成员列表
*
* <p>分页返回指定项目下的所有成员信息包含角色信息</p>
*
* @param projectId 项目唯一标识符
* @param page 页码从1开始
* @param size 每页数量
* @return 包含分页成员信息的成功响应
*/
@GetMapping("/{projectId}/members")
public ApiResponse<PageResponse<ProjectMemberVO>> getProjectMembers(
@PathVariable UUID projectId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
List<ProjectStaff> 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<ProjectMemberVO> pageData = staffList.subList(start, end).stream()
.map(ProjectMemberVO::fromEntity)
.collect(Collectors.toList());
PageResponse<ProjectMemberVO> response = new PageResponse<>(pageData, (long) staffList.size(), pageIndex + 1, size);
return ApiResponse.success(response);
}
/**
* 获取可添加到项目的成员列表企业员工
*
* <p>返回尚未加入该项目的所有企业员工列表支持模糊搜索用户</p>
*
* @param projectId 项目唯一标识符
* @param search 搜索关键字可选支持用户名和真实姓名模糊匹配
* @return 包含可用成员列表的成功响应
*/
@GetMapping("/{projectId}/available-members")
public ApiResponse<List<UserVO>> getAvailableMembers(
@PathVariable UUID projectId,
@RequestParam(required = false) String search) {
List<User> 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()));
}
/**
* 添加项目成员
*
* <p>将指定用户添加到项目中并分配多个角色</p>
*
* @param projectId 项目唯一标识符
* @param dto 添加项目成员的请求数据
* @return 成功响应
*/
@PostMapping("/{projectId}/members")
@OperationLog(operation = "添加项目成员", module = "PROJECT_MEMBER", action = AuditLog.ActionType.CREATE)
public ApiResponse<Void> addProjectMember(
@PathVariable UUID projectId,
@RequestBody AddProjectMemberDTO dto) {
userManagementService.assignStaffToProject(
dto.getUserId(),
projectId,
dto.getStaffType(),
dto.getRoleIds());
return ApiResponse.success();
}
/**
* 移除项目成员
*
* <p>将指定用户从项目中移除</p>
*
* @param projectId 项目唯一标识符
* @param userId 用户唯一标识符
* @return 成功响应
*/
@DeleteMapping("/{projectId}/members/{userId}")
@OperationLog(operation = "移除项目成员", module = "PROJECT_MEMBER", action = AuditLog.ActionType.DELETE)
public ApiResponse<Void> removeProjectMember(
@PathVariable UUID projectId,
@PathVariable UUID userId) {
userManagementService.removeStaffFromProject(userId, projectId);
return ApiResponse.success();
}
}

View File

@ -14,40 +14,108 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
/**
* 角色管理REST接口控制器
*
* <p>提供角色Role相关的HTTP API接口包括角色的增删改查权限分配用户关联等功能
* 所有接口均遵循RESTful设计规范返回统一的ApiResponse格式</p>
*
* <p>主要接口</p>
* <ul>
* <li>GET /api/auth/roles - 查询所有角色</li>
* <li>GET /api/auth/roles/{id} - 根据ID查询角色</li>
* <li>GET /api/auth/roles/project/{projectId} - 根据项目ID查询角色</li>
* <li>POST /api/auth/roles - 创建角色</li>
* <li>PUT /api/auth/roles/{id} - 更新角色</li>
* <li>DELETE /api/auth/roles/{id} - 删除角色</li>
* <li>POST /api/auth/roles/{id}/permissions - 为角色分配权限</li>
* <li>GET /api/auth/roles/{id}/permissions - 获取角色的权限</li>
* <li>GET /api/auth/roles/{id}/users - 获取拥有某角色的用户</li>
* </ul>
*
* @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<ApiResponse<List<Role>>> findAll() {
return ResponseEntity.ok(ApiResponse.success(roleService.findAll()));
}
/**
* 根据角色ID查询角色
*
* @param id 角色ID
* @return 包含角色信息的响应
*/
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Role>> findById(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(roleService.findById(id)));
}
/**
* 根据项目ID查询角色列表
*
* <p>获取指定项目下的所有角色</p>
*
* @param projectId 项目ID
* @return 包含该项目角色列表的响应
*/
@GetMapping("/project/{projectId}")
public ResponseEntity<ApiResponse<List<Role>>> findByProjectId(@PathVariable String projectId) {
return ResponseEntity.ok(ApiResponse.success(roleService.findByProjectId(projectId)));
}
/**
* 创建新角色
*
* <p>创建一个新的角色记录审计日志</p>
*
* @param role 要创建的角色信息请求体
* @return 包含创建后角色信息的响应
*/
@PostMapping
@OperationLog(operation = "创建角色", module = "ROLE", action = AuditLog.ActionType.CREATE)
public ResponseEntity<ApiResponse<Role>> create(@RequestBody Role role) {
return ResponseEntity.ok(ApiResponse.success(roleService.create(role)));
}
/**
* 更新角色信息
*
* <p>更新指定角色的信息记录审计日志</p>
*
* @param id 要更新的角色ID
* @param role 包含新数据的角色信息请求体
* @return 包含更新后角色信息的响应
*/
@PutMapping("/{id}")
@OperationLog(operation = "更新角色", module = "ROLE", action = AuditLog.ActionType.UPDATE)
public ResponseEntity<ApiResponse<Role>> update(@PathVariable UUID id, @RequestBody Role role) {
return ResponseEntity.ok(ApiResponse.success(roleService.update(id, role)));
}
/**
* 删除角色
*
* <p>删除指定的角色记录审计日志</p>
*
* @param id 要删除的角色ID
* @return 空响应表示操作成功
*/
@DeleteMapping("/{id}")
@OperationLog(operation = "删除角色", module = "ROLE", action = AuditLog.ActionType.DELETE)
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) {
@ -55,6 +123,15 @@ public class RoleController {
return ResponseEntity.ok(ApiResponse.success());
}
/**
* 为角色分配权限
*
* <p>替换角色原有的所有权限记录审计日志</p>
*
* @param id 角色ID
* @param permissionIds 要分配的权限ID列表请求体
* @return 空响应表示操作成功
*/
@PostMapping("/{id}/permissions")
@OperationLog(operation = "分配权限", module = "ROLE", action = AuditLog.ActionType.ASSIGN)
public ResponseEntity<ApiResponse<Void>> assignPermissions(
@ -64,11 +141,27 @@ public class RoleController {
return ResponseEntity.ok(ApiResponse.success());
}
/**
* 获取角色的所有权限
*
* <p>查询指定角色关联的所有权限</p>
*
* @param id 角色ID
* @return 包含角色权限列表的响应
*/
@GetMapping("/{id}/permissions")
public ResponseEntity<ApiResponse<List<Permission>>> getPermissions(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(roleService.getPermissions(id)));
}
/**
* 获取拥有某角色的所有用户
*
* <p>查询所有拥有指定角色ID的用户</p>
*
* @param id 角色ID
* @return 包含用户列表的响应
*/
@GetMapping("/{id}/users")
public ResponseEntity<ApiResponse<List<User>>> getUsersByRoleId(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(roleService.getUsersByRoleId(id)));

View File

@ -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<ApiResponse<Map<String, String>>> 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<ApiResponse<SysConfig>> 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<ApiResponse<SysConfig>> 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<ApiResponse<Map<String, String>>> updateConfigs(@RequestBody Map<String, String> configs) {
return ResponseEntity.ok(ApiResponse.success(sysConfigService.updateConfigs(configs)));
}
/**
* 配置更新请求对象
* 用于接收单个配置项的更新请求
*/
@Data
public static class ConfigUpdateRequest {
/**
* 新的配置值
*/
private String configValue;
}
}

View File

@ -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;
/**
* 用户管理控制器
*
* <p>提供用户相关的RESTful API接口包括用户CRUD密码管理角色分配项目关联等功能</p>
*
* <p>所有接口路径前缀为/api/auth/users使用JSON格式进行数据交互</p>
*
* @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;
/**
* 获取所有用户列表
*
* <p>返回系统中所有用户的信息包含关联的角色列表</p>
*
* @return 包含所有用户的成功响应
*/
@GetMapping
public ResponseEntity<ApiResponse<List<User>>> findAll() {
return ResponseEntity.ok(ApiResponse.success(userService.findAll()));
}
/**
* 根据ID获取用户信息
*
* <p>根据用户唯一标识符查询指定用户的详细信息</p>
*
* @param id 用户唯一标识符
* @return 包含用户信息的成功响应
*/
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> findById(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(userService.findById(id)));
}
/**
* 创建新用户
*
* <p>创建一个新的用户账号需要提供用户名密码等基本信息</p>
*
* @param user 待创建的用户信息
* @return 包含创建成功的用户信息的成功响应
*/
@PostMapping
@OperationLog(operation = "创建用户", module = "USER", action = AuditLog.ActionType.CREATE)
public ResponseEntity<ApiResponse<User>> create(@RequestBody User user) {
return ResponseEntity.ok(ApiResponse.success(userService.create(user)));
}
/**
* 更新用户信息
*
* <p>更新指定用户的个人信息如真实姓名手机号邮箱等</p>
*
* @param id 用户唯一标识符
* @param user 包含更新信息的用户对象
* @return 包含更新后用户信息的成功响应
*/
@PutMapping("/{id}")
@OperationLog(operation = "更新用户", module = "USER", action = AuditLog.ActionType.UPDATE)
public ResponseEntity<ApiResponse<User>> update(@PathVariable UUID id, @RequestBody User user) {
return ResponseEntity.ok(ApiResponse.success(userService.update(id, user)));
}
/**
* 删除用户
*
* <p>根据用户ID删除指定用户账号</p>
*
* @param id 用户唯一标识符
* @return 成功响应
*/
@DeleteMapping("/{id}")
@OperationLog(operation = "删除用户", module = "USER", action = AuditLog.ActionType.DELETE)
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable UUID id) {
@ -53,6 +111,15 @@ public class UserController {
return ResponseEntity.ok(ApiResponse.success());
}
/**
* 修改用户密码
*
* <p>用户修改自己的密码需要提供原密码和新密码</p>
*
* @param id 用户唯一标识符
* @param request 包含原密码和新密码的请求对象
* @return 成功响应
*/
@PutMapping("/{id}/password")
@OperationLog(operation = "修改密码", module = "USER", action = AuditLog.ActionType.UPDATE)
public ResponseEntity<ApiResponse<Void>> updatePassword(
@ -62,6 +129,15 @@ public class UserController {
return ResponseEntity.ok(ApiResponse.success());
}
/**
* 分配用户角色
*
* <p>为指定用户分配一个或多个角色替换现有的角色</p>
*
* @param id 用户唯一标识符
* @param roleIds 要分配的角色ID列表
* @return 成功响应
*/
@PostMapping("/{id}/roles")
@OperationLog(operation = "分配角色", module = "USER", action = AuditLog.ActionType.ASSIGN)
public ResponseEntity<ApiResponse<Void>> assignRoles(
@ -71,11 +147,28 @@ public class UserController {
return ResponseEntity.ok(ApiResponse.success());
}
/**
* 获取用户参与的项目列表
*
* <p>查询指定用户参与的所有项目及其在项目中的角色</p>
*
* @param id 用户唯一标识符
* @return 包含用户参与项目列表的成功响应
*/
@GetMapping("/{id}/projects")
public ResponseEntity<ApiResponse<List<UserProject>>> getUserProjects(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(userProjectService.getUserProjects(id)));
}
/**
* 添加用户到项目
*
* <p>将指定用户添加到某个项目中并设置用户在项目中的角色</p>
*
* @param id 用户唯一标识符
* @param request 包含项目ID和角色信息的请求对象
* @return 成功响应
*/
@PostMapping("/{id}/projects")
public ResponseEntity<ApiResponse<Void>> addUserToProject(
@PathVariable UUID id,
@ -84,6 +177,15 @@ public class UserController {
return ResponseEntity.ok(ApiResponse.success());
}
/**
* 从项目中移除用户
*
* <p>将指定用户从某个项目中移除删除用户与项目的关联关系</p>
*
* @param id 用户唯一标识符
* @param projectId 项目唯一标识符
* @return 成功响应
*/
@DeleteMapping("/{id}/projects/{projectId}")
public ResponseEntity<ApiResponse<Void>> removeUserFromProject(
@PathVariable UUID id,
@ -92,9 +194,29 @@ public class UserController {
return ResponseEntity.ok(ApiResponse.success());
}
/**
* 获取企业员工列表用于项目管理添加成员
*
* <p>返回所有企业员工的用户信息列表</p>
*
* @return 包含企业员工列表的成功响应
*/
@GetMapping("/enterprise")
public ResponseEntity<ApiResponse<List<UserVO>>> getEnterpriseUsers() {
List<User> users = userManagementService.findEnterpriseUsers();
return ResponseEntity.ok(ApiResponse.success(users.stream().map(UserVO::fromEntity).collect(Collectors.toList())));
}
/**
* 密码修改请求对象
*
* <p>包含修改密码所需的原密码和新密码</p>
*/
@Data
public static class PasswordRequest {
/** 原密码 */
private String oldPassword;
/** 新密码 */
private String newPassword;
}
}

View File

@ -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
*
* <p>用于接收将用户添加到项目的请求数据</p>
*
* @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<UUID> roleIds;
}

View File

@ -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
*
* <p>用于接收创建企业用户的请求数据包含用户基础信息和企业扩展信息</p>
*
* @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;
}

View File

@ -0,0 +1,63 @@
package com.ether.pms.auth.controller.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.UUID;
/**
* 部门数据传输对象
*
* <p>用于接收创建或更新部门的请求数据</p>
*
* @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";
}

View File

@ -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;
/**
* 部门视图对象
*
* <p>用于返回部门信息的视图对象包含部门基本信息和子部门列表</p>
*
* @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<DeptVO> 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 "行政管理";
}
}
}

View File

@ -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
*
* <p>用于返回分页数据的统一格式包含内容列表分页信息和总数</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResponse<T> {
/**
* 分页数据内容列表
*/
private List<T> content;
/**
* 当前页数据条数
*/
private int size;
/**
* 当前页码
*/
private int page;
/**
* 数据总条数
*/
private long totalElements;
/**
* 构造分页响应对象
*
* @param content 数据内容列表
* @param totalElements 数据总条数
* @param page 当前页码
* @param size 每页条数
*/
public PageResponse(List<T> content, long totalElements, int page, int size) {
this.content = content;
this.totalElements = totalElements;
this.page = page;
this.size = size;
}
}

View File

@ -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;
/**
* 项目成员视图对象
*
* <p>用于返回项目成员信息包含用户基本信息角色信息和加入时间</p>
*
* @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<String> 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<ProjectStaffRole> staffRoles = staff.getStaffRoles();
List<String> roleCodes = new ArrayList<>();
List<String> 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<com.ether.pms.auth.entity.Role> 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;
}
}

View File

@ -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;
/**
* 用户视图对象
*
* <p>用于返回用户信息的简化视图包含用户基本信息部门信息和角色列表</p>
*
* @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<RoleInfo> 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<RoleInfo> 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;
}
}

View File

@ -0,0 +1,131 @@
package com.ether.pms.auth.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 部门实体类
*
* <p>表示组织架构中的部门信息包含部门名称编码负责人类型等</p>
*
* <p>支持树形结构通过parentId指向上级部门</p>
*
* @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;
}
}
}

View File

@ -0,0 +1,72 @@
package com.ether.pms.auth.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDate;
import java.util.UUID;
/**
* 企业用户实体类
*
* <p>表示企业类型的用户信息包含员工的职位入职日期等企业相关信息</p>
*
* <p>与User实体为一对一关系共享主键</p>
*
* @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;
}

View File

@ -10,47 +10,101 @@ import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 权限实体类
*
* <p>用于定义系统中的权限信息包括权限代码名称类型资源路径等属性
* 权限是系统安全的基础单元用于控制用户对具体功能的访问能力</p>
*
* @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方法如GETPOSTPUTDELETE
*/
@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(
@ -60,16 +114,32 @@ public class Permission {
)
private List<Role> 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();

View File

@ -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;
/**
* 项目员工实体类
*
* <p>表示项目类型的员工信息包含所属项目员工类型班次等信息</p>
*
* <p>与User实体为一对一关系共享主键</p>
*
* @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<ProjectStaffRole> 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;
}

View File

@ -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();
}
}

View File

@ -0,0 +1,72 @@
package com.ether.pms.auth.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 住户实体类
*
* <p>表示住户类型的用户信息包含身份证认证状态等住户相关信息</p>
*
* <p>与User实体为一对一关系共享主键</p>
*
* @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;
}

View File

@ -0,0 +1,71 @@
package com.ether.pms.auth.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDate;
import java.util.UUID;
/**
* 住户房屋关联实体类
*
* <p>表示住户与房屋之间的关联关系记录住户与房屋的绑定信息</p>
*
* <p>支持业主家属租户等多种关系类型以及绑定状态管理</p>
*
* @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;
}

View File

@ -9,45 +9,92 @@ import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 角色实体类
*
* <p>用于定义系统中的角色信息包括角色代码名称描述类型数据范围等属性
* 角色与权限通过多对多关联一个角色可以拥有多个权限</p>
*
* @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",
@ -56,24 +103,47 @@ public class Role {
)
private List<Permission> 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;
@ -83,10 +153,18 @@ public class Role {
}
}
/**
* 数据范围枚举
* 定义角色可查看的数据范围级别
*/
public enum DataScope {
/** 全部数据,可查看所有数据 */
ALL("全部"),
/** 本项目数据,仅可查看所属项目数据 */
PROJECT("本项目"),
/** 本部门数据,仅可查看本部门数据 */
DEPARTMENT("本部门"),
/** 本 人数据,仅可查看本人数据 */
SELF("本人");
private final String desc;
@ -96,8 +174,14 @@ public class Role {
}
}
/**
* 角色状态枚举
* 定义角色的启用/禁用状态
*/
public enum RoleStatus {
/** 角色已启用,可以正常使用 */
ENABLED("启用"),
/** 角色已禁用,不可使用 */
DISABLED("禁用");
private final String desc;

View File

@ -0,0 +1,84 @@
package com.ether.pms.auth.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.math.BigDecimal;
import java.util.UUID;
/**
* 房屋空间实体类
*
* <p>表示项目中的房屋或空间信息包含楼栋单元房号等</p>
*
* <p>支持住宅和商业两种类型的空间管理</p>
*
* @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;
}

View File

@ -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;

View File

@ -10,48 +10,120 @@ import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 用户实体类
*
* <p>表示系统中的用户信息包含用户的基本认证信息和个人资料</p>
*
* <p>使用JPA注解映射到auth_user表支持基本的CRUD操作</p>
*
* @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",
@ -60,34 +132,72 @@ public class User {
)
private List<Role> 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;
}

View File

@ -5,27 +5,62 @@ import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 用户项目关联实体类
*
* <p>表示用户与项目之间的关联关系记录用户在特定项目中的角色和加入时间</p>
*
* <p>一个用户可以关联多个项目一个项目也可以关联多个用户形成多对多关系</p>
*
* @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) {

View File

@ -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;
/**
* 部门数据访问层接口
*
* <p>提供部门数据的持久化操作继承Spring Data JPA的JpaRepository接口</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Repository
public interface DeptRepository extends JpaRepository<Dept, UUID> {
/**
* 查询顶级部门列表
*
* <p>查询没有上级部门的部门即树形结构的根节点</p>
*
* @return 顶级部门列表
*/
List<Dept> findByParentIdIsNullOrderBySortOrder();
/**
* 根据父部门ID查询子部门列表
*
* @param parentId 父部门唯一标识符
* @return 该父部门下的所有子部门列表
*/
List<Dept> findByParentIdOrderBySortOrder(UUID parentId);
/**
* 根据部门编码查询部门
*
* @param deptCode 部门编码
* @return 包含部门的Optional对象
*/
Optional<Dept> findByDeptCode(String deptCode);
/**
* 根据部门类型查询部门列表
*
* @param deptType 部门类型
* @return 该类型的所有部门列表
*/
List<Dept> findByDeptTypeOrderBySortOrder(String deptType);
/**
* 查询所有启用的部门
*
* @return 所有状态为ACTIVE的部门
*/
List<Dept> findByStatusOrderBySortOrder(String status);
/**
* 根据部门负责人查询部门
*
* @param leaderId 负责人用户ID
* @return 该负责人管理的部门列表
*/
List<Dept> findByLeaderId(UUID leaderId);
/**
* 查询所有部门用于树形结构构建
*
* @return 所有部门列表
*/
@Query("SELECT d FROM Dept d ORDER BY d.sortOrder")
List<Dept> 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);
}

View File

@ -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;
/**
* 企业用户数据访问层接口
*
* <p>提供企业用户数据的持久化操作继承Spring Data JPA的JpaRepository接口</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Repository
public interface EnterpriseUserRepository extends JpaRepository<EnterpriseUser, UUID> {
/**
* 根据用户ID查询企业用户
*
* @param userId 用户唯一标识符
* @return 包含企业用户的Optional对象
*/
Optional<EnterpriseUser> findByUserId(UUID userId);
/**
* 根据部门ID查询企业用户列表
*
* @param deptId 部门唯一标识符
* @return 该部门下的所有企业用户列表
*/
List<EnterpriseUser> findByDeptId(UUID deptId);
}

View File

@ -6,12 +6,46 @@ import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
/**
* 权限数据访问层接口
*
* <p>提供权限Permission实体的数据库操作方法继承Spring Data JPA的JpaRepository
* 支持按权限类型父权限代码等条件查询</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Repository
public interface PermissionRepository extends JpaRepository<Permission, UUID> {
/**
* 根据权限类型查询权限列表
*
* <p>按权限类型筛选如MENU菜单BUTTON按钮API接口</p>
*
* @param type 权限类型
* @return 该类型的所有权限列表
*/
List<Permission> findByType(String type);
/**
* 根据父权限代码查询子权限列表
*
* <p>用于构建权限树形结构查找属于某个父权限的所有子权限</p>
*
* @param parentCode 父权限代码
* @return 该父权限下的所有子权限列表
*/
List<Permission> findByParentCode(String parentCode);
/**
* 检查权限代码是否存在
*
* <p>用于创建权限时的唯一性校验</p>
*
* @param code 权限代码
* @return 存在返回true不存在返回false
*/
boolean existsByCode(String code);
}

View File

@ -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;
/**
* 项目员工数据访问层接口
*
* <p>提供项目员工数据的持久化操作继承Spring Data JPA的JpaRepository接口</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Repository
public interface ProjectStaffRepository extends JpaRepository<ProjectStaff, UUID> {
/**
* 根据用户ID查询项目员工
*
* @param userId 用户唯一标识符
* @return 包含项目员工的Optional对象
*/
Optional<ProjectStaff> findByUserId(UUID userId);
/**
* 根据用户ID查询所有项目员工记录
*
* @param userId 用户唯一标识符
* @return 该用户的所有项目员工记录列表
*/
List<ProjectStaff> findAllByUserId(UUID userId);
/**
* 根据项目ID查询所有项目员工
*
* @param projectId 项目唯一标识符
* @return 该项目下的所有员工列表
*/
List<ProjectStaff> 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<ProjectStaff> findByProjectIdWithRoles(UUID projectId);
/**
* 根据用户ID和项目ID删除项目员工关联
*
* @param userId 用户唯一标识符
* @param projectId 项目唯一标识符
*/
void deleteByUserIdAndProjectId(UUID projectId, UUID userId);
/**
* 根据用户ID和项目ID查询项目员工
*
* @param userId 用户唯一标识符
* @param projectId 项目唯一标识符
* @return 包含项目员工的Optional对象
*/
Optional<ProjectStaff> findByUserIdAndProjectId(UUID userId, UUID projectId);
/**
* 根据项目ID统计成员数量
*
* @param projectId 项目唯一标识符
* @return 该项目的成员数量
*/
long countByProjectId(UUID projectId);
}

View File

@ -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<ProjectStaffRole, UUID> {
/**
* 根据员工ID查询所有角色关联
*
* @param staffId 员工ID
* @return 角色关联列表
*/
List<ProjectStaffRole> 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);
}

View File

@ -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;
/**
* 住户数据访问层接口
*
* <p>提供住户数据的持久化操作继承Spring Data JPA的JpaRepository接口</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Repository
public interface ResidentRepository extends JpaRepository<Resident, UUID> {
/**
* 根据用户ID查询住户
*
* @param userId 用户唯一标识符
* @return 包含住户的Optional对象
*/
Optional<Resident> findByUserId(UUID userId);
/**
* 根据认证状态查询住户列表
*
* @param status 认证状态
* @return 该状态下的所有住户列表
*/
List<Resident> findByVerificationStatus(String status);
}

View File

@ -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;
/**
* 住户房屋关联数据访问层接口
*
* <p>提供住户房屋关联数据的持久化操作继承Spring Data JPA的JpaRepository接口</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Repository
public interface ResidentSpaceRepository extends JpaRepository<ResidentSpace, UUID> {
/**
* 根据用户ID查询所有住户房屋关联
*
* @param userId 用户唯一标识符
* @return 该用户的所有住户房屋关联列表
*/
List<ResidentSpace> findByUserId(UUID userId);
/**
* 根据房屋ID查询所有住户房屋关联
*
* @param spaceId 房屋唯一标识符
* @return 该房屋的所有住户关联列表
*/
List<ResidentSpace> findBySpaceId(UUID spaceId);
/**
* 根据用户ID和房屋ID查询住户房屋关联
*
* @param userId 用户唯一标识符
* @param spaceId 房屋唯一标识符
* @return 包含住户房屋关联的Optional对象
*/
Optional<ResidentSpace> findByUserIdAndSpaceId(UUID userId, UUID spaceId);
}

View File

@ -8,17 +8,68 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* 角色数据访问层接口
*
* <p>提供角色Role实体的数据库操作方法继承Spring Data JPA的JpaRepository
* 支持按角色代码项目ID类型等条件查询以及带权限信息的复杂查询</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Repository
public interface RoleRepository extends JpaRepository<Role, UUID> {
/**
* 根据角色代码查询角色
*
* <p>角色代码在系统中唯一用于精确查找特定角色</p>
*
* @param code 角色代码
* @return 角色对象如果存在
*/
Optional<Role> findByCode(String code);
/**
* 检查角色代码是否存在
*
* <p>用于创建角色时的唯一性校验</p>
*
* @param code 角色代码
* @return 存在返回true不存在返回false
*/
boolean existsByCode(String code);
/**
* 根据项目ID查询角色列表
*
* <p>获取指定项目下的所有角色通常用于项目级别的角色筛选</p>
*
* @param projectId 项目ID
* @return 该项目下的角色列表
*/
List<Role> findByProjectId(String projectId);
/**
* 根据角色类型查询角色列表
*
* <p>按角色类型如系统级项目级部门级筛选角色</p>
*
* @param type 角色类型枚举
* @return 该类型的所有角色列表
*/
List<Role> findByType(Role.RoleType type);
/**
* 根据角色ID查询角色及其关联的权限信息
*
* <p>使用EntityGraph eagerly加载permissions关联数据
* 避免N+1查询问题提升查询性能</p>
*
* @param id 角色ID
* @return 包含权限信息的角色对象如果存在
*/
@EntityGraph(attributePaths = {"permissions"})
Optional<Role> findWithPermissionsById(UUID id);
}

View File

@ -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;
/**
* 房屋空间数据访问层接口
*
* <p>提供房屋空间数据的持久化操作继承Spring Data JPA的JpaRepository接口</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Repository
public interface SpaceRepository extends JpaRepository<Space, UUID> {
/**
* 根据项目ID查询所有房屋空间
*
* @param projectId 项目唯一标识符
* @return 该项目下的所有房屋空间列表
*/
List<Space> findByProjectId(UUID projectId);
/**
* 根据项目ID楼栋单元和房号查询房屋空间
*
* @param projectId 项目唯一标识符
* @param building 楼栋
* @param unit 单元
* @param roomNo 房号
* @return 包含房屋空间的Optional对象
*/
Optional<Space> findByProjectIdAndBuildingAndUnitAndRoomNo(UUID projectId, String building, String unit, String roomNo);
}

View File

@ -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<SysConfig, UUID> {
/**
* 根据配置键查询配置项
*
* @param configKey 配置键property_company_name
* @return 配置项 Optional 包装
*/
Optional<SysConfig> findByConfigKey(String configKey);
}

View File

@ -10,19 +10,85 @@ import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
/**
* 用户项目关联数据访问层接口
*
* <p>提供用户与项目关联关系的持久化操作继承Spring Data JPA的JpaRepository接口</p>
*
* <p>支持用户项目关系查询分页批量操作等功能</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Repository
public interface UserProjectRepository extends JpaRepository<UserProject, UUID> {
/**
* 根据用户ID查询该用户关联的所有项目
*
* <p>返回指定用户参与的所有项目关联记录列表</p>
*
* @param userId 用户唯一标识符
* @return 该用户关联的所有项目列表
*/
List<UserProject> findByUserId(UUID userId);
/**
* 根据项目ID查询该项目的所有关联用户
*
* <p>返回参与指定项目的所有用户关联记录列表</p>
*
* @param projectId 项目唯一标识符
* @return 该项目关联的所有用户列表
*/
List<UserProject> findByProjectId(UUID projectId);
/**
* 根据项目ID分页查询该项目的所有关联用户
*
* <p>支持分页展示项目成员列表</p>
*
* @param projectId 项目唯一标识符
* @param pageable 分页参数包含页码和每页大小
* @return 分页的用户项目关联列表
*/
Page<UserProject> findByProjectId(UUID projectId, Pageable pageable);
/**
* 根据用户ID查询该用户关联的所有项目ID
*
* <p>仅返回项目ID列表用于快速判断用户参与的项目</p>
*
* @param userId 用户唯一标识符
* @return 该用户关联的所有项目ID列表
*/
@Query("SELECT up.projectId FROM UserProject up WHERE up.userId = :userId")
List<UUID> findProjectIdsByUserId(@Param("userId") UUID userId);
/**
* 检查用户与项目的关联是否存在
*
* <p>用于判断用户是否参与了指定项目</p>
*
* @param userId 用户唯一标识符
* @param projectId 项目唯一标识符
* @return 存在返回true不存在返回false
*/
boolean existsByUserIdAndProjectId(UUID userId, UUID projectId);
/**
* 删除用户与项目的关联
*
* <p>根据用户ID和项目ID删除关联记录用于用户退出项目或移除项目成员</p>
*
* @param userId 用户唯一标识符
* @param projectId 项目唯一标识符
*/
void deleteByUserIdAndProjectId(UUID userId, UUID projectId);
/**
* 统计项目下的成员数量
*/
long countByProjectId(UUID projectId);
}

View File

@ -9,24 +9,121 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* 用户数据访问层接口
*
* <p>提供用户数据的持久化操作继承Spring Data JPA的JpaRepository接口</p>
*
* <p>包含用户查询角色关联查询等数据库操作方法</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
/**
* 根据用户名查询用户
*
* <p>最基础的按用户名查询方法返回Optional包装的用户对象</p>
*
* @param username 用户名
* @return 包含用户的Optional对象如果不存在则为空
*/
Optional<User> findByUsername(String username);
/**
* 根据用户名查询用户及其关联的角色
*
* <p>使用LEFT JOIN FETCH预加载用户的角色信息避免N+1查询问题</p>
*
* @param username 用户名
* @return 包含用户及其角色的Optional对象
*/
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username")
Optional<User> findByUsernameWithRoles(@Param("username") String username);
/**
* 查询所有用户及其关联的角色
*
* <p>一次性加载所有用户及其角色信息适用于管理后台用户列表</p>
*
* @return 包含所有用户及其角色的列表
*/
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles")
List<User> findAllWithRoles();
/**
* 根据ID查询用户及其关联的角色
*
* <p>使用LEFT JOIN FETCH预加载用户的角色信息</p>
*
* @param id 用户唯一标识符
* @return 包含用户及其角色的Optional对象
*/
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.id = :id")
Optional<User> findByIdWithRoles(@Param("id") UUID id);
/**
* 根据角色ID查询所有拥有该角色的用户
*
* <p>用于查询具有特定角色的所有用户列表</p>
*
* @param roleId 角色唯一标识符
* @return 拥有该角色的所有用户列表
*/
@Query("SELECT u FROM User u JOIN u.roles r WHERE r.id = :roleId")
List<User> findByRoleId(@Param("roleId") UUID roleId);
/**
* 检查用户名是否存在
*
* <p>用于用户注册时验证用户名唯一性</p>
*
* @param username 用户名
* @return 存在返回true不存在返回false
*/
boolean existsByUsername(String username);
/**
* 检查手机号是否存在
*
* <p>用于用户注册或更新时验证手机号唯一性</p>
*
* @param phone 手机号码
* @return 存在返回true不存在返回false
*/
boolean existsByPhone(String phone);
/**
* 根据用户类型查询用户列表
*
* <p>用于查询特定类型的用户如ENTERPRISEPROJECT_STAFF等</p>
*
* @param userType 用户类型
* @return 该类型的所有用户列表
*/
List<User> findByUserType(String userType);
/**
* 根据部门ID查询用户列表
*
* <p>用于查询属于特定部门的所有用户</p>
*
* @param deptId 部门唯一标识符
* @return 该部门下的所有用户列表
*/
List<User> findByDeptId(UUID deptId);
/**
* 根据项目ID查询项目员工
*
* <p>通过关联ProjectStaff表查询属于指定项目的所有员工用户</p>
*
* @param projectId 项目唯一标识符
* @return该项目下的所有员工用户列表
*/
@Query("SELECT u FROM User u JOIN ProjectStaff ps ON u.id = ps.userId WHERE ps.projectId = :projectId")
List<User> findProjectStaffsByProjectId(@Param("projectId") UUID projectId);
}

View File

@ -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;
/**
* 部门服务层
*
* <p>提供部门相关的业务逻辑处理包括部门树查询部门CRUD部门员工查询等功能</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class DeptService {
private final DeptRepository deptRepository;
private final UserRepository userRepository;
/**
* 获取部门树
*
* <p>返回所有部门的列表支持前端构建树形结构</p>
*
* @return 所有部门的列表
*/
public List<Dept> getDeptTree() {
return deptRepository.findAllForTree();
}
/**
* 获取所有启用的部门列表
*
* @return 启用的部门列表
*/
public List<Dept> getActiveDepts() {
return deptRepository.findByStatusOrderBySortOrder("ACTIVE");
}
/**
* 根据ID获取部门
*
* @param id 部门ID
* @return 部门对象
*/
public Optional<Dept> getById(UUID id) {
return deptRepository.findById(id);
}
/**
* 获取部门及下属员工
*
* <p>根据部门ID查询该部门下的所有员工用户</p>
*
* @param deptId 部门唯一标识符
* @return 该部门下的所有员工用户列表
*/
public List<User> 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<Dept> children = deptRepository.findByParentIdOrderBySortOrder(id);
if (!children.isEmpty()) {
throw new IllegalStateException("请先删除子部门");
}
deptRepository.delete(dept);
}
/**
* 根据部门类型查询部门
*
* @param deptType 部门类型
* @return 该类型的部门列表
*/
public List<Dept> getByType(String deptType) {
return deptRepository.findByDeptTypeOrderBySortOrder(deptType);
}
/**
* 检查部门编码是否存在
*
* @param deptCode 部门编码
* @return 存在返回true
*/
public boolean existsByCode(String deptCode) {
return deptRepository.existsByDeptCode(deptCode);
}
}

View File

@ -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,14 +11,15 @@ 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;
@ -45,13 +48,26 @@ public class LoginService {
loginAttemptService.recordSuccess(username);
Map<String, Object> claims = new HashMap<>();
Set<String> allRoles = new HashSet<>();
if (user.getRoles() != null) {
claims.put("roles", user.getRoles().stream()
allRoles.addAll(user.getRoles().stream()
.map(r -> r.getCode())
.toList());
}
List<ProjectStaff> 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<String, Object> claims = new HashMap<>();
claims.put("roles", new ArrayList<>(allRoles));
String token = jwtTokenProvider.generateToken(user.getId(), user.getUsername(), claims);
Map<String, Object> result = new HashMap<>();
@ -59,6 +75,7 @@ public class LoginService {
result.put("userId", user.getId());
result.put("username", user.getUsername());
result.put("realName", user.getRealName());
result.put("roles", new ArrayList<>(allRoles));
return result;
}

View File

@ -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("密码必须包含数字");
}

View File

@ -11,33 +11,91 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
/**
* 权限业务逻辑服务类
*
* <p>提供权限Permission相关的业务操作包括权限的增删改查类型筛选菜单权限获取等功能
* 所有写操作均支持事务管理确保数据一致性</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class PermissionService {
/** 权限数据访问仓库 */
private final PermissionRepository permissionRepository;
/**
* 查询所有权限
*
* @return 所有权限的列表
*/
public List<Permission> findAll() {
return permissionRepository.findAll();
}
/**
* 根据权限ID查询权限
*
* <p>如果权限不存在抛出PERMISSION_002业务异常</p>
*
* @param id 权限ID
* @return 权限对象
* @throws BusinessException 如果权限不存在
*/
public Permission findById(UUID id) {
return permissionRepository.findById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.PERMISSION_002));
}
/**
* 根据权限类型查询权限列表
*
* <p>按权限类型筛选如MENUBUTTONAPI等</p>
*
* @param type 权限类型
* @return 该类型的所有权限列表
*/
public List<Permission> findByType(String type) {
return permissionRepository.findByType(type);
}
/**
* 根据父权限代码查询子权限列表
*
* <p>用于获取某个父权限下的所有子权限构建权限树</p>
*
* @param parentCode 父权限代码
* @return 该父权限下的所有子权限列表
*/
public List<Permission> findByParentCode(String parentCode) {
return permissionRepository.findByParentCode(parentCode);
}
/**
* 查询所有菜单类型权限
*
* <p>获取type为MENU的所有权限通常用于前端菜单渲染</p>
*
* @return 所有菜单权限列表
*/
public List<Permission> findMenuPermissions() {
return permissionRepository.findByType("MENU");
}
/**
* 创建新权限
*
* <p>如果权限代码已存在抛出PERMISSION_001业务异常
* 创建时会自动设置创建时间和更新时间</p>
*
* @param permission 要创建的权限对象
* @return 创建后的权限对象包含数据库生成的主键
* @throws BusinessException 如果权限代码已存在
*/
@Transactional
public Permission create(Permission permission) {
if (permissionRepository.existsByCode(permission.getCode())) {
@ -46,6 +104,17 @@ public class PermissionService {
return permissionRepository.save(permission);
}
/**
* 更新权限信息
*
* <p>仅更新传入参数中非空的字段保持其他字段不变
* 如果权限不存在抛出PERMISSION_002业务异常</p>
*
* @param id 要更新的权限ID
* @param permission 包含新数据的权限对象
* @return 更新后的权限对象
* @throws BusinessException 如果权限不存在
*/
@Transactional
public Permission update(UUID id, Permission permission) {
Permission existing = findById(id);
@ -75,6 +144,13 @@ public class PermissionService {
return permissionRepository.save(existing);
}
/**
* 删除权限
*
* <p>根据权限ID删除权限记录</p>
*
* @param id 要删除的权限ID
*/
@Transactional
public void delete(UUID id) {
permissionRepository.deleteById(id);

View File

@ -15,32 +15,88 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
/**
* 角色业务逻辑服务类
*
* <p>提供角色Role相关的业务操作包括角色的增删改查权限分配用户关联等功能
* 所有操作均支持事务管理确保数据一致性</p>
*
* @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<Role> findAll() {
return roleRepository.findAll();
}
/**
* 根据角色ID查询角色
*
* <p>如果角色不存在抛出ROLE_002业务异常</p>
*
* @param id 角色ID
* @return 角色对象
* @throws BusinessException 如果角色不存在
*/
public Role findById(UUID id) {
return roleRepository.findById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.ROLE_002));
}
/**
* 根据角色代码查询角色
*
* <p>如果角色不存在抛出ROLE_002业务异常</p>
*
* @param code 角色代码
* @return 角色对象
* @throws BusinessException 如果角色不存在
*/
public Role findByCode(String code) {
return roleRepository.findByCode(code)
.orElseThrow(() -> new BusinessException(ErrorCode.ROLE_002));
}
/**
* 根据项目ID查询角色列表
*
* <p>获取指定项目下的所有角色</p>
*
* @param projectId 项目ID
* @return 该项目下的角色列表
*/
public List<Role> findByProjectId(String projectId) {
return roleRepository.findByProjectId(projectId);
}
/**
* 创建新角色
*
* <p>如果角色代码已存在抛出ROLE_001业务异常
* 创建时会自动设置创建时间和更新时间</p>
*
* @param role 要创建的角色对象
* @return 创建后的角色对象包含数据库生成的主键
* @throws BusinessException 如果角色代码已存在
*/
@Transactional
public Role create(Role role) {
if (roleRepository.existsByCode(role.getCode())) {
@ -49,6 +105,17 @@ public class RoleService {
return roleRepository.save(role);
}
/**
* 更新角色信息
*
* <p>仅更新传入参数中非空的字段保持其他字段不变
* 如果角色不存在抛出ROLE_002业务异常</p>
*
* @param id 要更新的角色ID
* @param role 包含新数据的角色对象
* @return 更新后的角色对象
* @throws BusinessException 如果角色不存在
*/
@Transactional
public Role update(UUID id, Role role) {
Role existing = findById(id);
@ -75,6 +142,16 @@ public class RoleService {
return roleRepository.save(existing);
}
/**
* 为角色分配权限
*
* <p>替换角色原有的所有权限为新分配的权限列表
* 如果角色或权限不存在抛出相应业务异常</p>
*
* @param roleId 角色ID
* @param permissionIds 要分配的权限ID列表
* @throws BusinessException 如果角色或权限不存在
*/
@Transactional
public void assignPermissions(UUID roleId, List<UUID> permissionIds) {
Role role = findById(roleId);
@ -83,18 +160,60 @@ public class RoleService {
roleRepository.save(role);
}
/**
* 删除角色
*
* <p>根据角色ID删除角色记录</p>
*
* @param id 要删除的角色ID
*/
@Transactional
public void delete(UUID id) {
roleRepository.deleteById(id);
}
/**
* 获取角色的所有权限
*
* <p>使用EntityGraph eagerly加载权限信息避免N+1查询
* 如果角色不存在抛出ROLE_002业务异常</p>
*
* @param roleId 角色ID
* @return 该角色关联的所有权限列表
* @throws BusinessException 如果角色不存在
*/
public List<Permission> getPermissions(UUID roleId) {
Role role = roleRepository.findWithPermissionsById(roleId)
.orElseThrow(() -> new BusinessException(ErrorCode.ROLE_002));
return role.getPermissions();
}
/**
* 根据角色ID查询关联的用户列表
*
* <p>获取拥有该角色的所有用户</p>
*
* @param roleId 角色ID
* @return 拥有该角色的用户列表
*/
public List<User> getUsersByRoleId(UUID roleId) {
return userRepository.findByRoleId(roleId);
}
/**
* 为用户分配角色
*
* <p>根据角色编码查找角色并分配给用户</p>
*
* @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);
}
}

View File

@ -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 配置键值对 Mapkey configKeyvalue configValue
*/
public Map<String, String> getAllConfigs() {
List<SysConfig> configs = sysConfigRepository.findAll();
Map<String, String> 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<String, String> updateConfigs(Map<String, String> configs) {
Map<String, String> result = new HashMap<>();

View File

@ -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;
/**
* 用户管理服务层
*
* <p>提供用户管理的核心业务逻辑包括企业员工管理项目员工分配等功能</p>
*
* <p>作为用户管理的核心服务供其他模块调用</p>
*
* @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;
/**
* 查询所有企业员工
*
* <p>返回用户类型为ENTERPRISE的所有用户列表</p>
*
* @return 所有企业用户的列表
*/
public List<User> findEnterpriseUsers() {
return userRepository.findByUserType("ENTERPRISE");
}
/**
* 查询项目员工
*
* <p>根据项目ID查询该项目下的所有员工</p>
*
* @param projectId 项目唯一标识符
* @return 该项目下的所有员工列表
*/
public List<User> findProjectStaffs(UUID projectId) {
return userRepository.findProjectStaffsByProjectId(projectId);
}
/**
* 创建企业员工
*
* <p>创建基础用户信息和企业员工扩展信息包括工号部门职位等
* 自动分配该部门的默认角色到用户</p>
*
* @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);
}
}
/**
* 分配员工到项目
*
* <p>将用户分配到指定项目设定员工类型和分配状态支持多角色分配</p>
*
* @param userId 用户唯一标识符
* @param projectId 项目唯一标识符
* @param staffType 员工类型
* @param roleIds 角色ID列表
* @return 创建的项目员工关联记录
*/
@Transactional
public ProjectStaff assignStaffToProject(UUID userId, UUID projectId, String staffType, List<UUID> 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<ProjectStaffRole> 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;
}
/**
* 查询项目成员
*
* <p>根据项目ID查询该项目下的所有成员用户</p>
*
* @param projectId 项目唯一标识符
* @return 该项目下的所有成员用户列表
*/
public List<User> getProjectMembers(UUID projectId) {
return userRepository.findProjectStaffsByProjectId(projectId);
}
/**
* 查询项目员工列表包含角色信息
*
* <p>根据项目ID查询该项目下的所有员工信息包含角色信息</p>
*
* @param projectId 项目唯一标识符
* @return 该项目下的所有员工列表
*/
public List<ProjectStaff> getProjectStaffsWithRoles(UUID projectId) {
List<ProjectStaff> staffs = projectStaffRepository.findByProjectIdWithRoles(projectId);
for (ProjectStaff staff : staffs) {
Hibernate.initialize(staff.getUser().getRoles());
}
return staffs;
}
/**
* 从项目移除员工
*
* <p>将用户从指定项目中移除删除项目员工关联关系</p>
*
* @param userId 用户唯一标识符
* @param projectId 项目唯一标识符
*/
@Transactional
public void removeStaffFromProject(UUID userId, UUID projectId) {
Optional<ProjectStaff> staff = projectStaffRepository.findByUserIdAndProjectId(userId, projectId);
staff.ifPresent(s -> {
// 级联删除角色关联通过 orphanRemoval
projectStaffRepository.delete(s);
});
}
}

View File

@ -8,20 +8,52 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
/**
* 用户项目关联服务层
*
* <p>提供用户与项目关联关系的业务逻辑处理包括关联查询添加移除等功能</p>
*
* @author Ether开发团队
* @version 1.0.0
* @since 2024-01-01
*/
@Service
@RequiredArgsConstructor
public class UserProjectService {
/** 用户项目关联数据访问接口 */
private final UserProjectRepository userProjectRepository;
/**
* 获取用户参与的所有项目
*
* <p>根据用户ID查询该用户参与的所有项目关联记录</p *
* @param userId 用户唯一标识符
* @return 用户参与的所有项目关联列表
*/
public List<UserProject> getUserProjects(UUID userId) {
return userProjectRepository.findByUserId(userId);
}
/**
* 获取用户参与的所有项目ID
*
* <p>根据用户ID查询该用户参与的所有项目ID列表用于权限验证等场景</p *
* @param userId 用户唯一标识符
* @return 用户参与的所有项目ID列表
*/
public List<UUID> getUserProjectIds(UUID userId) {
return userProjectRepository.findProjectIdsByUserId(userId);
}
/**
* 添加用户到项目
*
* <p>创建用户与项目之间的关联关系如果已存在关联则不重复创建</p *
* @param userId 用户唯一标识符
* @param projectId 项目唯一标识符
* @param role 用户在项目中的角色默认为"member"
*/
@Transactional
public void addUserToProject(UUID userId, UUID projectId, String role) {
if (userProjectRepository.existsByUserIdAndProjectId(userId, projectId)) {
@ -34,11 +66,26 @@ public class UserProjectService {
userProjectRepository.save(up);
}
/**
* 从项目中移除用户
*
* <p>删除用户与项目之间的关联关系</p *
* @param userId 用户唯一标识符
* @param projectId 项目唯一标识符
*/
@Transactional
public void removeUserFromProject(UUID userId, UUID projectId) {
userProjectRepository.deleteByUserIdAndProjectId(userId, projectId);
}
/**
* 检查用户是否在项目中
*
* <p>判断用户与项目之间是否存在关联关系</p *
* @param userId 用户唯一标识符
* @param projectId 项目唯一标识符
* @return 存在关联返回true否则返回false
*/
public boolean isUserInProject(UUID userId, UUID projectId) {
return userProjectRepository.existsByUserIdAndProjectId(userId, projectId);
}

View File

@ -14,28 +14,76 @@ import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 用户服务层
*
* <p>提供用户相关的业务逻辑处理包括用户CRUD密码管理角色分配等功能</p>
*
* <p>所有涉及数据修改的操作均添加事务注解保证数据一致性</p>
*
* @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;
/**
* 查询所有用户
*
* <p>返回所有用户及其关联的角色信息</p>
*
* @return 所有用户的列表
*/
public List<User> findAll() {
return userRepository.findAllWithRoles();
}
/**
* 根据ID查询用户
*
* <p>根据用户唯一标识符查询用户信息</p>
*
* @param id 用户唯一标识符
* @return 用户对象
* @throws BusinessException 如果用户不存在抛出业务异常
*/
public User findById(UUID id) {
return userRepository.findById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_003));
}
/**
* 根据用户名查询用户
*
* <p>根据用户名精确查询用户信息用于登录验证等场景</p>
*
* @param username 用户名
* @return 用户对象
* @throws BusinessException 如果用户不存在抛出业务异常
*/
public User findByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_003));
}
/**
* 创建新用户
*
* <p>创建新用户并对密码进行加密存储同时验证用户名和手机号的唯一性</p>
*
* @param user 待创建的用户对象
* @return 创建成功的用户对象包含数据库生成的ID
* @throws BusinessException 如果用户名已存在手机号已存在或密码不符合要求
*/
@Transactional
public User create(User user) {
if (userRepository.existsByUsername(user.getUsername())) {
@ -60,6 +108,16 @@ public class UserService {
return userRepository.save(user);
}
/**
* 更新用户信息
*
* <p>根据用户ID更新用户信息仅更新非空字段</p>
*
* @param id 用户唯一标识符
* @param user 包含更新信息的用户对象
* @return 更新后的用户对象
* @throws BusinessException 如果用户不存在或手机号已被其他用户使用
*/
@Transactional
public User update(UUID id, User user) {
User existing = findById(id);
@ -86,6 +144,15 @@ public class UserService {
return userRepository.save(existing);
}
/**
* 修改用户密码
*
* <p>用户主动修改密码需验证原密码正确性新密码需符合强度要求</p *
* @param id 用户唯一标识符
* @param oldPassword 原密码
* @param newPassword 新密码
* @throws BusinessException 如果原密码错误或新密码不符合要求
*/
@Transactional
public void updatePassword(UUID id, String oldPassword, String newPassword) {
User user = findById(id);
@ -108,6 +175,14 @@ public class UserService {
userRepository.save(user);
}
/**
* 重置用户密码
*
* <p>管理员重置用户密码新密码需符合强度要求</p *
* @param id 用户唯一标识符
* @param newPassword 新密码
* @throws BusinessException 如果新密码不符合要求
*/
@Transactional
public void resetPassword(UUID id, String newPassword) {
User user = findById(id);
@ -126,6 +201,14 @@ public class UserService {
userRepository.save(user);
}
/**
* 分配用户角色
*
* <p>为用户分配一个或多个角色替换用户现有的所有角色</p *
* @param userId 用户唯一标识符
* @param roleIds 要分配的角色ID列表
* @throws BusinessException 如果用户不存在或任何角色ID无效
*/
@Transactional
public void assignRoles(UUID userId, List<UUID> roleIds) {
User user = findById(userId);
@ -134,11 +217,24 @@ public class UserService {
userRepository.save(user);
}
/**
* 删除用户
*
* <p>根据用户ID删除用户记录</p *
* @param id 用户唯一标识符
*/
@Transactional
public void delete(UUID id) {
userRepository.deleteById(id);
}
/**
* 更新用户登录信息
*
* <p>记录用户登录的时间戳和IP地址用于安全审计</p *
* @param userId 用户唯一标识符
* @param ip 登录IP地址
*/
public void updateLoginInfo(UUID userId, String ip) {
User user = findById(userId);
user.setLastLoginTime(LocalDateTime.now());

View File

@ -19,7 +19,7 @@ import java.util.UUID;
@Component
public class JwtTokenProvider {
@Value("${jwt.secret:ether-pms-secret-key-must-be-at-least-256-bits}")
@Value("${jwt.secret:#{systemEnvironment['JWT_SECRET'] ?: 'ether-pms-secret-key-must-be-at-least-256-bits'}}")
private String secret;
@Value("${jwt.expiration:86400000}")

View File

@ -0,0 +1,248 @@
package com.ether.pms.auth.controller.dto;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.Test;
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;
class ProjectMemberVOTest {
@Test
void fromEntity_shouldMapAllFieldsCorrectly() {
// Given
UUID userId = UUID.randomUUID();
UUID projectId = UUID.randomUUID();
UUID roleId = UUID.randomUUID();
LocalDateTime createdAt = LocalDateTime.of(2026, 4, 5, 10, 30);
User user = new User();
user.setId(userId);
user.setUsername("testuser");
user.setRealName("测试用户");
user.setPhone("13800138000");
user.setEmail("test@example.com");
user.setUserType("ENTERPRISE");
user.setStatus(User.UserStatus.ACTIVE);
Role role = new Role();
role.setId(roleId);
role.setCode("PROJECT_ADMIN");
role.setName("项目管理员");
ProjectStaffRole staffRole = new ProjectStaffRole();
staffRole.setRole(role);
ProjectStaff staff = new ProjectStaff();
staff.setId(UUID.randomUUID());
staff.setUser(user);
staff.setProjectId(projectId);
staff.setStaffType("SECURITY");
staff.setCreatedAt(createdAt);
staff.setStaffRoles(new ArrayList<>(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());
}
}

View File

@ -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<UUID> 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<UUID> 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<UUID> 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<UUID> 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<UUID> 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<UUID> 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<UUID> 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<UUID> 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<ProjectStaff> 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<ProjectStaff> 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());
}
}

View File

@ -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,16 +14,32 @@ public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> 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<ApiResponse<Void>> 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<ApiResponse<Void>> handleIllegalStateException(IllegalStateException e) {
log.warn("状态异常: {}", e.getMessage());
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(400, e.getMessage()));
}
@ -30,7 +47,7 @@ public class GlobalExceptionHandler {
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity
.status(HttpStatus.OK)
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(ErrorCode.SYSTEM_ERROR.getCode(), "系统错误,请稍后重试"));
}
}

View File

@ -60,6 +60,12 @@
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -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<EnergyMeter> createMeter(@Valid @RequestBody EnergyMeter meter) {
return ApiResponse.success(energyMeterService.createMeter(meter));
}
@GetMapping("/energy-meters")
@GetMapping("/meters")
public ApiResponse<List<EnergyMeter>> 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<EnergyMeter> getMeter(@PathVariable UUID id) {
return ApiResponse.success(energyMeterService.getMeterById(id));
}
@PutMapping("/energy-meters/{id}")
@PutMapping("/meters/{id}")
public ApiResponse<EnergyMeter> updateMeter(@PathVariable UUID id, @Valid @RequestBody EnergyMeter meter) {
return ApiResponse.success(energyMeterService.updateMeter(id, meter));
}
@DeleteMapping("/energy-meters/{id}")
@DeleteMapping("/meters/{id}")
public ApiResponse<Void> 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<EnergyConsumption> 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<List<EnergyConsumption>> 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<Map<EnergyMeter.EnergyType, BigDecimal>> 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<BigDecimal> getUnitConsumption(
@RequestParam UUID projectId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate month) {

View File

@ -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<InspectionItem> createItem(@RequestBody InspectionItem item) {
return ApiResponse.success(inspectionItemService.createItem(item));
}
@GetMapping
public ApiResponse<List<InspectionItem>> getItems(
@RequestParam(required = false) String equipmentType,
@RequestParam(required = false) String systemType,
@RequestParam(required = false) Boolean activeOnly) {
List<InspectionItem> 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<InspectionItem> getItem(@PathVariable UUID id) {
return ApiResponse.success(inspectionItemService.getItemById(id));
}
@PutMapping("/{id}")
public ApiResponse<InspectionItem> updateItem(@PathVariable UUID id, @RequestBody InspectionItem item) {
return ApiResponse.success(inspectionItemService.updateItem(id, item));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteItem(@PathVariable UUID id) {
inspectionItemService.deleteItem(id);
return ApiResponse.success(null);
}
}

View File

@ -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<InspectionRecord> createRecord(@RequestBody InspectionRecord record) {
return ApiResponse.success(inspectionRecordService.createRecord(record));
}
@GetMapping
public ApiResponse<List<InspectionRecord>> 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<InspectionRecord> 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<InspectionRecord> getRecord(@PathVariable UUID id) {
return ApiResponse.success(inspectionRecordService.getRecordById(id));
}
@PutMapping("/{id}")
public ApiResponse<InspectionRecord> updateRecord(@PathVariable UUID id, @RequestBody InspectionRecord record) {
return ApiResponse.success(inspectionRecordService.updateRecord(id, record));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteRecord(@PathVariable UUID id) {
inspectionRecordService.deleteRecord(id);
return ApiResponse.success(null);
}
@PostMapping("/{id}/complete")
public ApiResponse<InspectionRecord> completeRecord(@PathVariable UUID id) {
return ApiResponse.success(inspectionRecordService.completeRecord(id));
}
}

View File

@ -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 {

View File

@ -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<MaintenancePlan> createPlan(@Valid @RequestBody MaintenancePlan plan) {
MaintenancePlan created = maintenancePlanService.createPlan(plan);
return ApiResponse.success(created);
}
@GetMapping
public ApiResponse<List<MaintenancePlan>> getPlans(
@RequestParam UUID projectId,
@RequestParam(required = false) MaintenancePlan.TriggerType triggerType) {
List<MaintenancePlan> plans;
if (triggerType != null) {
plans = maintenancePlanService.getPlansByTriggerType(triggerType);
} else {
plans = maintenancePlanService.getActivePlansByProject(projectId);
}
return ApiResponse.success(plans);
}
@GetMapping("/{id}")
public ApiResponse<MaintenancePlan> getPlan(@PathVariable UUID id) {
return ApiResponse.success(maintenancePlanService.getPlanById(id));
}
@PutMapping("/{id}")
public ApiResponse<MaintenancePlan> updatePlan(@PathVariable UUID id, @Valid @RequestBody MaintenancePlan plan) {
return ApiResponse.success(maintenancePlanService.updatePlan(id, plan));
}
@DeleteMapping("/{id}")
public ApiResponse<Void> deactivatePlan(@PathVariable UUID id) {
maintenancePlanService.deactivatePlan(id);
return ApiResponse.success(null);
}
}

Some files were not shown because too many files have changed in this diff Show More