feat: 文档整理+代码修复

- 工单分页查询(P0 OOM修复)
- 前后端枚举统一(维保触发类型/任务状态)
- Permission增加菜单路由属性(path/component/icon/visible)
- 工单状态机补全(SUSPENDED/RETURNED/previousStatus)
- SpaceNode增加code字段+自动编码规则
- 设备模型统一(SpaceNode设备扩展字段@Deprecated)
- WorkOrder逻辑删除(isDeleted)
- 健康评分Bug修复(SpaceNode→Equipment, projectId硬编码→动态)
- DataAccess降级@Deprecated
- 健康评分标注Beta
- PasswordServiceTest/UserServiceTest方法名修复
- module-wo pom.xml添加test依赖
- RoleController参数类型修复
This commit is contained in:
chiguyong 2026-05-18 10:46:32 +08:00
parent 680ebe932c
commit 34c51288db
23 changed files with 369 additions and 101 deletions

View File

@ -25,17 +25,20 @@ public class EquipmentHealthController {
@GetMapping("/{equipmentId}") @GetMapping("/{equipmentId}")
public ApiResponse<EquipmentHealthScore> getEquipmentHealth(@PathVariable UUID equipmentId) { public ApiResponse<EquipmentHealthScore> getEquipmentHealth(@PathVariable UUID equipmentId) {
return ApiResponse.success(equipmentHealthService.getLatestHealthScore(equipmentId)); EquipmentHealthScore score = equipmentHealthService.getLatestHealthScore(equipmentId);
return ApiResponse.success("[Beta] 健康评分数据准确性待验证", score);
} }
@GetMapping("/{equipmentId}/history") @GetMapping("/{equipmentId}/history")
public ApiResponse<List<EquipmentHealthScore>> getHealthHistory(@PathVariable UUID equipmentId) { public ApiResponse<List<EquipmentHealthScore>> getHealthHistory(@PathVariable UUID equipmentId) {
return ApiResponse.success(equipmentHealthService.getHealthHistory(equipmentId)); List<EquipmentHealthScore> history = equipmentHealthService.getHealthHistory(equipmentId);
return ApiResponse.success("[Beta] 健康评分数据准确性待验证", history);
} }
@PostMapping("/calculate") @PostMapping("/calculate")
public ApiResponse<EquipmentHealthScore> calculateHealthScore(@Valid @RequestBody CalculateHealthRequest request) { public ApiResponse<EquipmentHealthScore> calculateHealthScore(@Valid @RequestBody CalculateHealthRequest request) {
return ApiResponse.success(equipmentHealthService.calculateHealthScore(request.getEquipmentId())); EquipmentHealthScore score = equipmentHealthService.calculateHealthScore(request.getEquipmentId());
return ApiResponse.success("[Beta] 健康评分数据准确性待验证", score);
} }
@PostMapping("/failure-history") @PostMapping("/failure-history")

View File

@ -7,6 +7,9 @@ import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.UUID; import java.util.UUID;
/**
* 此功能为Beta版本数据准确性待验证
*/
@Entity @Entity
@Table(name = "ops_equipment_health_score", indexes = { @Table(name = "ops_equipment_health_score", indexes = {
@Index(name = "idx_health_equipment", columnList = "equipment_id"), @Index(name = "idx_health_equipment", columnList = "equipment_id"),

View File

@ -1,12 +1,12 @@
package com.ether.pms.asset.service.impl; package com.ether.pms.asset.service.impl;
import com.ether.pms.common.BusinessException; import com.ether.pms.common.BusinessException;
import com.ether.pms.asset.entity.Equipment;
import com.ether.pms.asset.entity.EquipmentFailureHistory; import com.ether.pms.asset.entity.EquipmentFailureHistory;
import com.ether.pms.asset.entity.EquipmentHealthScore; import com.ether.pms.asset.entity.EquipmentHealthScore;
import com.ether.pms.mdm.entity.SpaceNode;
import com.ether.pms.asset.repository.EquipmentFailureHistoryRepository; import com.ether.pms.asset.repository.EquipmentFailureHistoryRepository;
import com.ether.pms.asset.repository.EquipmentHealthScoreRepository; import com.ether.pms.asset.repository.EquipmentHealthScoreRepository;
import com.ether.pms.mdm.repository.SpaceNodeRepository; import com.ether.pms.asset.repository.EquipmentRepository;
import com.ether.pms.asset.service.EquipmentHealthService; import com.ether.pms.asset.service.EquipmentHealthService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -29,7 +29,7 @@ public class EquipmentHealthServiceImpl implements EquipmentHealthService {
private final EquipmentFailureHistoryRepository failureHistoryRepository; private final EquipmentFailureHistoryRepository failureHistoryRepository;
private final EquipmentHealthScoreRepository healthScoreRepository; private final EquipmentHealthScoreRepository healthScoreRepository;
private final SpaceNodeRepository spaceNodeRepository; private final EquipmentRepository equipmentRepository;
// TODO: 需要改为从 ops 模块查询工单数据 // TODO: 需要改为从 ops 模块查询工单数据
// private final MaintenanceTaskRepository maintenanceTaskRepository; // private final MaintenanceTaskRepository maintenanceTaskRepository;
@ -43,13 +43,9 @@ public class EquipmentHealthServiceImpl implements EquipmentHealthService {
@Override @Override
@Transactional @Transactional
public EquipmentHealthScore calculateHealthScore(UUID equipmentId) { public EquipmentHealthScore calculateHealthScore(UUID equipmentId) {
SpaceNode equipment = spaceNodeRepository.findByIdAndIsDeletedFalse(equipmentId) Equipment equipment = equipmentRepository.findByIdAndIsDeletedFalse(equipmentId)
.orElseThrow(() -> new BusinessException(6001, "设备不存在")); .orElseThrow(() -> new BusinessException(6001, "设备不存在"));
if (!Boolean.TRUE.equals(equipment.getIsEquipment())) {
throw new BusinessException(6002, "该空间节点不是设备");
}
// 获取近30天故障次数 // 获取近30天故障次数
LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30); LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);
long failureCount30d = failureHistoryRepository.countByEquipmentIdSince(equipmentId, thirtyDaysAgo); long failureCount30d = failureHistoryRepository.countByEquipmentIdSince(equipmentId, thirtyDaysAgo);
@ -86,9 +82,9 @@ public class EquipmentHealthServiceImpl implements EquipmentHealthService {
// 创建健康度记录 // 创建健康度记录
EquipmentHealthScore health = new EquipmentHealthScore(); EquipmentHealthScore health = new EquipmentHealthScore();
health.setProjectId(UUID.fromString("00000000-0000-0000-0000-000000000000")); // 需要根据projectCode查询 health.setProjectId(equipment.getProjectId());
health.setEquipmentId(equipmentId); health.setEquipmentId(equipmentId);
health.setEquipmentName(equipment.getName()); health.setEquipmentName(equipment.getEquipmentName());
health.setHealthScore(healthScore.setScale(2, RoundingMode.HALF_UP)); health.setHealthScore(healthScore.setScale(2, RoundingMode.HALF_UP));
health.setFailureDeduction(failureDeduction.setScale(2, RoundingMode.HALF_UP)); health.setFailureDeduction(failureDeduction.setScale(2, RoundingMode.HALF_UP));
health.setMaintenanceDeduction(maintenanceDeduction.setScale(2, RoundingMode.HALF_UP)); health.setMaintenanceDeduction(maintenanceDeduction.setScale(2, RoundingMode.HALF_UP));
@ -167,21 +163,17 @@ public class EquipmentHealthServiceImpl implements EquipmentHealthService {
@Transactional @Transactional
public EquipmentFailureHistory recordFailure(EquipmentFailureHistory failure) { public EquipmentFailureHistory recordFailure(EquipmentFailureHistory failure) {
// 校验设备存在 // 校验设备存在
SpaceNode equipment = spaceNodeRepository.findByIdAndIsDeletedFalse(failure.getEquipmentId()) Equipment equipment = equipmentRepository.findByIdAndIsDeletedFalse(failure.getEquipmentId())
.orElseThrow(() -> new BusinessException(6001, "设备不存在")); .orElseThrow(() -> new BusinessException(6001, "设备不存在"));
if (!Boolean.TRUE.equals(equipment.getIsEquipment())) {
throw new BusinessException(6002, "该空间节点不是设备");
}
// 设置项目ID // 设置项目ID
if (failure.getProjectId() == null) { if (failure.getProjectId() == null) {
failure.setProjectId(UUID.fromString("00000000-0000-0000-0000-000000000000")); // 需要根据projectCode查询 failure.setProjectId(equipment.getProjectId());
} }
// 设置设备信息 // 设置设备信息
if (failure.getEquipmentName() == null) { if (failure.getEquipmentName() == null) {
failure.setEquipmentName(equipment.getName()); failure.setEquipmentName(equipment.getEquipmentName());
} }
// 计算修复时长 // 计算修复时长
@ -219,8 +211,8 @@ public class EquipmentHealthServiceImpl implements EquipmentHealthService {
/** /**
* 计算设备年龄 * 计算设备年龄
*/ */
private BigDecimal calculateEquipmentAge(SpaceNode equipment) { private BigDecimal calculateEquipmentAge(Equipment equipment) {
LocalDate installationDate = equipment.getMaintenanceContractStart(); LocalDate installationDate = equipment.getInstallationDate();
if (installationDate == null) { if (installationDate == null) {
// 如果没有安装日期使用创建日期 // 如果没有安装日期使用创建日期
if (equipment.getCreatedAt() != null) { if (equipment.getCreatedAt() != null) {

View File

@ -21,6 +21,7 @@ public class DataAccessController {
private final DataAccessService dataAccessService; private final DataAccessService dataAccessService;
@Deprecated
@PostMapping @PostMapping
public ResponseEntity<ApiResponse<Void>> grantAccess(@Valid @RequestBody DataAccessRequest request) { public ResponseEntity<ApiResponse<Void>> grantAccess(@Valid @RequestBody DataAccessRequest request) {
UUID currentUserId = SecurityUtils.getCurrentUserId(); UUID currentUserId = SecurityUtils.getCurrentUserId();
@ -38,12 +39,14 @@ public class DataAccessController {
return ResponseEntity.ok(ApiResponse.success()); return ResponseEntity.ok(ApiResponse.success());
} }
@Deprecated
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> revokeAccess(@PathVariable UUID id) { public ResponseEntity<ApiResponse<Void>> revokeAccess(@PathVariable UUID id) {
dataAccessService.revokeAccess(id); dataAccessService.revokeAccess(id);
return ResponseEntity.ok(ApiResponse.success()); return ResponseEntity.ok(ApiResponse.success());
} }
@Deprecated
@GetMapping @GetMapping
public ResponseEntity<ApiResponse<List<DataAccess>>> getDataAccess( public ResponseEntity<ApiResponse<List<DataAccess>>> getDataAccess(
@RequestParam String dataType, @RequestParam String dataType,

View File

@ -8,6 +8,7 @@ import com.ether.pms.auth.entity.User;
import com.ether.pms.auth.service.RoleService; import com.ether.pms.auth.service.RoleService;
import com.ether.pms.common.ApiResponse; import com.ether.pms.common.ApiResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -87,7 +88,7 @@ public class RoleController {
* @return 包含该项目角色列表的响应 * @return 包含该项目角色列表的响应
*/ */
@GetMapping("/project/{projectId}") @GetMapping("/project/{projectId}")
public ResponseEntity<ApiResponse<List<Role>>> findByProjectId(@PathVariable String projectId) { public ResponseEntity<ApiResponse<List<Role>>> findByProjectId(@PathVariable UUID projectId) {
return ResponseEntity.ok(ApiResponse.success(roleService.findByProjectId(projectId))); return ResponseEntity.ok(ApiResponse.success(roleService.findByProjectId(projectId)));
} }

View File

@ -8,6 +8,7 @@ import java.util.UUID;
@Entity @Entity
@Table(name = "biz_data_access") @Table(name = "biz_data_access")
@Data @Data
@Deprecated
public class DataAccess { public class DataAccess {
@Id @Id

View File

@ -98,8 +98,23 @@ public class Permission {
* 排序序号 * 排序序号
* 用于前端展示时的排序数字越小越靠前 * 用于前端展示时的排序数字越小越靠前
*/ */
@Size(max = 200, message = "路由路径长度不能超过200位")
@Column(length = 200)
private String path;
@Size(max = 200, message = "组件路径长度不能超过200位")
@Column(length = 200)
private String component;
@Size(max = 100, message = "图标名称长度不能超过100位")
@Column(length = 100)
private String icon;
private Integer sortOrder; private Integer sortOrder;
@Column(nullable = false)
private Boolean visible = true;
/** /**
* 权限关联的角色列表 * 权限关联的角色列表
* 多对多关系通过auth_role_permission关联表维护 * 多对多关系通过auth_role_permission关联表维护

View File

@ -33,14 +33,14 @@ class PasswordServiceTest {
} }
@Test @Test
void isPasswordWeak_shouldReturnTrue_forCommonWeakPasswords() { void isWeakPassword_shouldReturnTrue_forCommonWeakPasswords() {
assertTrue(passwordService.isPasswordWeak("password123")); assertTrue(passwordService.isWeakPassword("password123"));
assertTrue(passwordService.isPasswordWeak("admin123")); assertTrue(passwordService.isWeakPassword("admin123"));
assertTrue(passwordService.isPasswordWeak("qwerty123")); assertTrue(passwordService.isWeakPassword("qwerty123"));
} }
@Test @Test
void isPasswordWeak_shouldReturnFalse_forStrongPasswords() { void isWeakPassword_shouldReturnFalse_forStrongPasswords() {
assertFalse(passwordService.isPasswordWeak("Str0ng!Pass")); assertFalse(passwordService.isWeakPassword("Str0ng!Pass"));
} }
} }

View File

@ -4,6 +4,7 @@ import com.ether.pms.auth.entity.User;
import com.ether.pms.auth.repository.RoleRepository; import com.ether.pms.auth.repository.RoleRepository;
import com.ether.pms.auth.repository.UserRepository; import com.ether.pms.auth.repository.UserRepository;
import com.ether.pms.common.BusinessException; import com.ether.pms.common.BusinessException;
import com.ether.pms.common.ErrorCode;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
@ -86,8 +87,8 @@ class UserServiceTest {
newUser.setPassword("weak"); newUser.setPassword("weak");
when(userRepository.existsByUsername("newuser")).thenReturn(false); when(userRepository.existsByUsername("newuser")).thenReturn(false);
doThrow(new IllegalArgumentException("密码太弱")) doThrow(new BusinessException(ErrorCode.PASSWORD_001))
.when(passwordService).validatePassword("weak"); .when(passwordService).validateStrength("weak");
assertThrows(BusinessException.class, () -> userService.create(newUser)); assertThrows(BusinessException.class, () -> userService.create(newUser));
} }
@ -99,8 +100,8 @@ class UserServiceTest {
newUser.setPassword("Valid123!"); newUser.setPassword("Valid123!");
when(userRepository.existsByUsername("newuser")).thenReturn(false); when(userRepository.existsByUsername("newuser")).thenReturn(false);
doNothing().when(passwordService).validatePassword("Valid123!"); doNothing().when(passwordService).validateStrength("Valid123!");
when(passwordService.isPasswordWeak("Valid123!")).thenReturn(false); when(passwordService.isWeakPassword("Valid123!")).thenReturn(false);
when(passwordService.encode("Valid123!")).thenReturn("encodedPassword"); when(passwordService.encode("Valid123!")).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenReturn(newUser); when(userRepository.save(any(User.class))).thenReturn(newUser);

View File

@ -125,7 +125,9 @@ public class SpaceNodeController {
/** /**
* 获取设备详情 * 获取设备详情
* @deprecated 请使用 /api/asset/equipment 替代
*/ */
@Deprecated
@GetMapping("/{id}/equipment") @GetMapping("/{id}/equipment")
public ResponseEntity<ApiResponse<SpaceNodeEquipmentDTO>> getEquipment(@PathVariable UUID id) { public ResponseEntity<ApiResponse<SpaceNodeEquipmentDTO>> getEquipment(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.getEquipmentById(id))); return ResponseEntity.ok(ApiResponse.success(spaceNodeService.getEquipmentById(id)));
@ -133,7 +135,9 @@ public class SpaceNodeController {
/** /**
* 获取设备列表 * 获取设备列表
* @deprecated 请使用 /api/asset/equipment 替代
*/ */
@Deprecated
@GetMapping("/equipment") @GetMapping("/equipment")
public ResponseEntity<ApiResponse<List<SpaceNodeEquipmentDTO>>> getEquipmentList( public ResponseEntity<ApiResponse<List<SpaceNodeEquipmentDTO>>> getEquipmentList(
@RequestParam UUID projectId) { @RequestParam UUID projectId) {
@ -142,7 +146,9 @@ public class SpaceNodeController {
/** /**
* 获取特种设备列表 * 获取特种设备列表
* @deprecated 请使用 /api/asset/equipment 替代
*/ */
@Deprecated
@GetMapping("/special-equipment") @GetMapping("/special-equipment")
public ResponseEntity<ApiResponse<List<SpaceNodeEquipmentDTO>>> getSpecialEquipment( public ResponseEntity<ApiResponse<List<SpaceNodeEquipmentDTO>>> getSpecialEquipment(
@RequestParam UUID projectId) { @RequestParam UUID projectId) {
@ -151,7 +157,9 @@ public class SpaceNodeController {
/** /**
* 获取即将年检设备 * 获取即将年检设备
* @deprecated 请使用 /api/asset/equipment 替代
*/ */
@Deprecated
@GetMapping("/expiring-inspection") @GetMapping("/expiring-inspection")
public ResponseEntity<ApiResponse<List<SpaceNodeEquipmentDTO>>> getExpiringInspection( public ResponseEntity<ApiResponse<List<SpaceNodeEquipmentDTO>>> getExpiringInspection(
@RequestParam UUID projectId, @RequestParam UUID projectId,
@ -161,7 +169,9 @@ public class SpaceNodeController {
/** /**
* 创建设备 * 创建设备
* @deprecated 请使用 /api/asset/equipment 替代
*/ */
@Deprecated
@PostMapping("/equipment") @PostMapping("/equipment")
public ResponseEntity<ApiResponse<SpaceNode>> createEquipment(@Valid @RequestBody EquipmentCreateDTO dto) { public ResponseEntity<ApiResponse<SpaceNode>> createEquipment(@Valid @RequestBody EquipmentCreateDTO dto) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.createEquipment(dto))); return ResponseEntity.ok(ApiResponse.success(spaceNodeService.createEquipment(dto)));
@ -169,7 +179,9 @@ public class SpaceNodeController {
/** /**
* 批量创建设备 * 批量创建设备
* @deprecated 请使用 /api/asset/equipment 替代
*/ */
@Deprecated
@PostMapping("/equipment/batch") @PostMapping("/equipment/batch")
public ResponseEntity<ApiResponse<List<SpaceNode>>> batchCreateEquipment(@Valid @RequestBody List<EquipmentCreateDTO> dtoList) { public ResponseEntity<ApiResponse<List<SpaceNode>>> batchCreateEquipment(@Valid @RequestBody List<EquipmentCreateDTO> dtoList) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.batchCreateEquipment(dtoList))); return ResponseEntity.ok(ApiResponse.success(spaceNodeService.batchCreateEquipment(dtoList)));
@ -177,7 +189,9 @@ public class SpaceNodeController {
/** /**
* Excel导入设备 * Excel导入设备
* @deprecated 请使用 /api/asset/equipment 替代
*/ */
@Deprecated
@PostMapping("/equipment/import") @PostMapping("/equipment/import")
public ResponseEntity<ApiResponse<Object>> importEquipment( public ResponseEntity<ApiResponse<Object>> importEquipment(
@RequestParam("file") MultipartFile file, @RequestParam("file") MultipartFile file,

View File

@ -13,6 +13,8 @@ public class SpaceNodeCreateDTO {
@NotNull(message = "项目ID不能为空") @NotNull(message = "项目ID不能为空")
private UUID projectId; private UUID projectId;
private String code;
@NotBlank(message = "空间节点名称不能为空") @NotBlank(message = "空间节点名称不能为空")
private String name; private String name;

View File

@ -1,5 +1,6 @@
package com.ether.pms.mdm.entity; package com.ether.pms.mdm.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
@ -32,6 +33,10 @@ public class SpaceNode {
@Column(name = "project_code", nullable = false, length = 50) @Column(name = "project_code", nullable = false, length = 50)
private UUID projectId; private UUID projectId;
@JsonIgnore
@Column(name = "code", length = 50)
private String code;
@NotNull(message = "空间节点名称不能为空") @NotNull(message = "空间节点名称不能为空")
@Size(min = 1, max = 100, message = "空间节点名称长度必须在1-100位之间") @Size(min = 1, max = 100, message = "空间节点名称长度必须在1-100位之间")
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
@ -137,67 +142,88 @@ public class SpaceNode {
@Column(name = "is_deleted") @Column(name = "is_deleted")
private Boolean isDeleted = false; private Boolean isDeleted = false;
// ========== 设备扩展字段 ========== // ========== 设备扩展字段@Deprecated请使用 module-asset Equipment 实体 ==========
@Deprecated
@Column(name = "is_equipment") @Column(name = "is_equipment")
private Boolean isEquipment = false; private Boolean isEquipment = false;
@Deprecated
@Column(name = "design_life_years") @Column(name = "design_life_years")
private Integer designLifeYears; private Integer designLifeYears;
@Deprecated
@Column(name = "rated_power", precision = 10, scale = 2) @Column(name = "rated_power", precision = 10, scale = 2)
private BigDecimal ratedPower; private BigDecimal ratedPower;
@Deprecated
@Column(name = "rated_voltage", precision = 10, scale = 2) @Column(name = "rated_voltage", precision = 10, scale = 2)
private BigDecimal ratedVoltage; private BigDecimal ratedVoltage;
@Deprecated
@Column(name = "rated_current", precision = 10, scale = 2) @Column(name = "rated_current", precision = 10, scale = 2)
private BigDecimal ratedCurrent; private BigDecimal ratedCurrent;
@Deprecated
@Column(name = "maintenance_vendor", length = 100) @Column(name = "maintenance_vendor", length = 100)
private String maintenanceVendor; private String maintenanceVendor;
@Deprecated
@Column(name = "maintenance_vendor_contact", length = 50) @Column(name = "maintenance_vendor_contact", length = 50)
private String maintenanceVendorContact; private String maintenanceVendorContact;
@Deprecated
@Column(name = "maintenance_vendor_phone", length = 20) @Column(name = "maintenance_vendor_phone", length = 20)
private String maintenanceVendorPhone; private String maintenanceVendorPhone;
@Deprecated
@Column(name = "maintenance_contract_no", length = 50) @Column(name = "maintenance_contract_no", length = 50)
private String maintenanceContractNo; private String maintenanceContractNo;
@Deprecated
@Column(name = "maintenance_contract_start") @Column(name = "maintenance_contract_start")
private LocalDate maintenanceContractStart; private LocalDate maintenanceContractStart;
@Deprecated
@Column(name = "maintenance_contract_end") @Column(name = "maintenance_contract_end")
private LocalDate maintenanceContractEnd; private LocalDate maintenanceContractEnd;
@Deprecated
@Column(name = "special_equipment_type", length = 50) @Column(name = "special_equipment_type", length = 50)
private String specialEquipmentType; private String specialEquipmentType;
@Deprecated
@Column(name = "special_equipment_cert", length = 100) @Column(name = "special_equipment_cert", length = 100)
private String specialEquipmentCert; private String specialEquipmentCert;
@Deprecated
@Column(name = "inspection_cycle") @Column(name = "inspection_cycle")
private Integer inspectionCycle; private Integer inspectionCycle;
@Deprecated
@Column(name = "next_inspection_date") @Column(name = "next_inspection_date")
private LocalDate nextInspectionDate; private LocalDate nextInspectionDate;
@Deprecated
@Column(name = "last_inspection_date") @Column(name = "last_inspection_date")
private LocalDate lastInspectionDate; private LocalDate lastInspectionDate;
@Deprecated
@Column(name = "last_inspection_result", length = 20) @Column(name = "last_inspection_result", length = 20)
private String lastInspectionResult; private String lastInspectionResult;
@Deprecated
@Column(name = "common_spare_parts", length = 2000) @Column(name = "common_spare_parts", length = 2000)
private String commonSpareParts; private String commonSpareParts;
@Deprecated
@Column(name = "energy_consumption_standard", precision = 12, scale = 2) @Column(name = "energy_consumption_standard", precision = 12, scale = 2)
private BigDecimal energyConsumptionStandard; private BigDecimal energyConsumptionStandard;
@Deprecated
@Column(name = "installation_environment", length = 50) @Column(name = "installation_environment", length = 50)
private String installationEnvironment; private String installationEnvironment;
@Deprecated
@Column(name = "protection_level", length = 20) @Column(name = "protection_level", length = 20)
private String protectionLevel; private String protectionLevel;
// ========== 设备扩展字段结束 ========== // ========== 设备扩展字段结束 ==========

View File

@ -65,4 +65,7 @@ public interface SpaceNodeRepository extends JpaRepository<SpaceNode, UUID> {
* 统计项目下指定类型空间数量 * 统计项目下指定类型空间数量
*/ */
long countByProjectIdAndNodeTypeAndIsDeletedFalse(UUID projectId, SpaceNode.NodeType nodeType); long countByProjectIdAndNodeTypeAndIsDeletedFalse(UUID projectId, SpaceNode.NodeType nodeType);
@Query("SELECT MAX(sn.code) FROM SpaceNode sn WHERE sn.projectId = :projectId AND sn.nodeType = :nodeType AND sn.code LIKE CONCAT(:prefix, '%') AND sn.isDeleted = false")
Optional<String> findMaxCodeByProjectAndTypeAndPrefix(@Param("projectId") UUID projectId, @Param("nodeType") SpaceNode.NodeType nodeType, @Param("prefix") String prefix);
} }

View File

@ -10,7 +10,9 @@ import com.ether.pms.mdm.dto.SpaceNodeUpdateDTO;
import com.ether.pms.mdm.dto.FloorInfoVO; import com.ether.pms.mdm.dto.FloorInfoVO;
import com.ether.pms.mdm.dto.FloorDetailVO; import com.ether.pms.mdm.dto.FloorDetailVO;
import com.ether.pms.mdm.entity.SpaceNode; import com.ether.pms.mdm.entity.SpaceNode;
import com.ether.pms.mdm.entity.Project;
import com.ether.pms.mdm.repository.SpaceNodeRepository; import com.ether.pms.mdm.repository.SpaceNodeRepository;
import com.ether.pms.mdm.repository.ProjectRepository;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -27,6 +29,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -40,6 +43,7 @@ import com.ether.pms.mdm.dto.EquipmentCreateDTO;
public class SpaceNodeService { public class SpaceNodeService {
private final SpaceNodeRepository spaceNodeRepository; private final SpaceNodeRepository spaceNodeRepository;
private final ProjectRepository projectRepository;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
/** /**
@ -142,6 +146,13 @@ public class SpaceNodeService {
node.setNodeCategory(nodeType.getCategory()); node.setNodeCategory(nodeType.getCategory());
node.setNodeType(nodeType); node.setNodeType(nodeType);
} }
if (dto.getCode() != null && !dto.getCode().isBlank()) {
node.setCode(dto.getCode());
} else {
node.setCode(generateCode(dto.getProjectId(), node.getNodeType()));
}
node.setUsageType(dto.getUsageType()); node.setUsageType(dto.getUsageType());
node.setSortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0); node.setSortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0);
node.setStatus(dto.getStatus() != null ? dto.getStatus() : "ACTIVE"); node.setStatus(dto.getStatus() != null ? dto.getStatus() : "ACTIVE");
@ -734,6 +745,46 @@ public class SpaceNodeService {
/** /**
* 楼栋楼层配置从JSON解析 * 楼栋楼层配置从JSON解析
*/ */
private String generateCode(UUID projectId, SpaceNode.NodeType nodeType) {
Project project = projectRepository.findById(projectId)
.orElseThrow(() -> new BusinessException(ErrorCode.SPACE_002));
String projSuffix = project.getCode().length() > 4
? project.getCode().substring(project.getCode().length() - 4)
: project.getCode();
String prefix = getCodePrefix(nodeType);
String codePrefix = prefix + "-" + projSuffix + "-";
Optional<String> maxCode = spaceNodeRepository.findMaxCodeByProjectAndTypeAndPrefix(
projectId, nodeType, codePrefix);
int nextSeq = 1;
if (maxCode.isPresent() && maxCode.get() != null) {
String max = maxCode.get();
String seqPart = max.substring(codePrefix.length());
try {
nextSeq = Integer.parseInt(seqPart) + 1;
} catch (NumberFormatException e) {
nextSeq = 1;
}
}
return codePrefix + String.format("%04d", nextSeq);
}
private String getCodePrefix(SpaceNode.NodeType nodeType) {
return switch (nodeType) {
case BUILDING -> "BLD";
case UNIT -> "UNT";
case FLOOR -> "FLR";
case ROOM -> "RM";
default -> nodeType.name().length() >= 3
? nodeType.name().substring(0, 3)
: nodeType.name();
};
}
@Data @Data
public static class BuildingFloorConfig { public static class BuildingFloorConfig {
private Integer totalFloors; private Integer totalFloors;

View File

@ -65,5 +65,11 @@
<groupId>org.mapstruct</groupId> <groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId> <artifactId>mapstruct</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -2,6 +2,7 @@ package com.ether.pms.ops.controller;
import com.ether.pms.common.ApiResponse; import com.ether.pms.common.ApiResponse;
import com.ether.pms.common.util.BatchOperationValidator; import com.ether.pms.common.util.BatchOperationValidator;
import com.ether.pms.mdm.dto.PageResponse;
import com.ether.pms.ops.dto.WorkOrderStatsDTO; import com.ether.pms.ops.dto.WorkOrderStatsDTO;
import com.ether.pms.ops.entity.WorkOrder; import com.ether.pms.ops.entity.WorkOrder;
import com.ether.pms.ops.entity.WorkOrderItem; import com.ether.pms.ops.entity.WorkOrderItem;
@ -31,30 +32,19 @@ public class WorkOrderController {
} }
@GetMapping @GetMapping
public ApiResponse<List<WorkOrder>> list( public ApiResponse<PageResponse<WorkOrder>> list(
@RequestParam(required = false) UUID projectId, @RequestParam(required = false) UUID projectId,
@RequestParam(required = false) UUID equipmentId, @RequestParam(required = false) UUID equipmentId,
@RequestParam(required = false) WorkOrder.Source source, @RequestParam(required = false) WorkOrder.Source source,
@RequestParam(required = false) WorkOrder.Type type, @RequestParam(required = false) WorkOrder.Type type,
@RequestParam(required = false) WorkOrder.Status status, @RequestParam(required = false) WorkOrder.Status status,
@RequestParam(required = false) String assignedTo) { @RequestParam(required = false) WorkOrder.Priority priority,
List<WorkOrder> list; @RequestParam(required = false) String assignedTo,
if (projectId != null) { @RequestParam(required = false) String keyword,
list = workOrderService.getWorkOrdersByProject(projectId); @RequestParam(defaultValue = "0") int page,
} else if (equipmentId != null) { @RequestParam(defaultValue = "20") int size) {
list = workOrderService.getWorkOrdersByEquipment(equipmentId); return ApiResponse.success(workOrderService.queryWorkOrders(
} else if (source != null) { projectId, equipmentId, source, type, status, priority, assignedTo, keyword, page, size));
list = workOrderService.getWorkOrdersBySource(source);
} else if (type != null) {
list = workOrderService.getWorkOrdersByType(type);
} else if (status != null) {
list = workOrderService.getWorkOrdersByStatus(status);
} else if (assignedTo != null) {
list = workOrderService.getWorkOrdersByAssignedTo(assignedTo);
} else {
list = workOrderService.getAllWorkOrders();
}
return ApiResponse.success(list);
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@ -98,6 +88,21 @@ public class WorkOrderController {
return ApiResponse.success(workOrderService.cancelWorkOrder(id)); return ApiResponse.success(workOrderService.cancelWorkOrder(id));
} }
@PostMapping("/{id}/suspend")
public ApiResponse<WorkOrder> suspend(@PathVariable UUID id) {
return ApiResponse.success(workOrderService.suspendWorkOrder(id));
}
@PostMapping("/{id}/resume")
public ApiResponse<WorkOrder> resume(@PathVariable UUID id) {
return ApiResponse.success(workOrderService.resumeWorkOrder(id));
}
@PostMapping("/{id}/return")
public ApiResponse<WorkOrder> returnOrder(@PathVariable UUID id) {
return ApiResponse.success(workOrderService.returnWorkOrder(id));
}
@GetMapping("/stats") @GetMapping("/stats")
public ApiResponse<WorkOrderStatsDTO> stats() { public ApiResponse<WorkOrderStatsDTO> stats() {
return ApiResponse.success(workOrderService.getWorkOrderStats()); return ApiResponse.success(workOrderService.getWorkOrderStats());

View File

@ -16,6 +16,8 @@ public class WorkOrderStatsDTO {
private long completed; private long completed;
private long verified; private long verified;
private long cancelled; private long cancelled;
private long suspended;
private long returned;
private long completedToday; private long completedToday;
private long createdToday; private long createdToday;
private long overdue; private long overdue;

View File

@ -57,9 +57,13 @@ public class WorkOrder {
private Status status = Status.PENDING; private Status status = Status.PENDING;
public enum Status { public enum Status {
PENDING, ASSIGNED, IN_PROGRESS, COMPLETED, VERIFIED, CANCELLED PENDING, ASSIGNED, IN_PROGRESS, SUSPENDED, RETURNED, COMPLETED, VERIFIED, CANCELLED
} }
@Column(name = "previous_status", length = 20)
@Enumerated(EnumType.STRING)
private Status previousStatus;
@Column(nullable = false, length = 200) @Column(nullable = false, length = 200)
private String title; private String title;
@ -147,6 +151,9 @@ public class WorkOrder {
@Column(length = 2000) @Column(length = 2000)
private String signature; private String signature;
@Column(name = "is_deleted")
private Boolean isDeleted = false;
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@ -160,6 +167,9 @@ public class WorkOrder {
public void prePersist() { public void prePersist() {
createdAt = LocalDateTime.now(); createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now(); updatedAt = LocalDateTime.now();
if (isDeleted == null) {
isDeleted = false;
}
} }
@PreUpdate @PreUpdate

View File

@ -1,7 +1,10 @@
package com.ether.pms.ops.repository; package com.ether.pms.ops.repository;
import com.ether.pms.ops.entity.WorkOrder; import com.ether.pms.ops.entity.WorkOrder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@ -13,51 +16,87 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Repository @Repository
public interface WorkOrderRepository extends JpaRepository<WorkOrder, UUID> { public interface WorkOrderRepository extends JpaRepository<WorkOrder, UUID>, JpaSpecificationExecutor<WorkOrder> {
Optional<WorkOrder> findByWorkNo(String workNo); Optional<WorkOrder> findByWorkNoAndIsDeletedFalse(String workNo);
List<WorkOrder> findByProjectId(UUID projectId); List<WorkOrder> findByProjectIdAndIsDeletedFalse(UUID projectId);
List<WorkOrder> findByEquipmentId(UUID equipmentId); List<WorkOrder> findByEquipmentIdAndIsDeletedFalse(UUID equipmentId);
List<WorkOrder> findBySource(WorkOrder.Source source); List<WorkOrder> findBySourceAndIsDeletedFalse(WorkOrder.Source source);
List<WorkOrder> findByType(WorkOrder.Type type); List<WorkOrder> findByTypeAndIsDeletedFalse(WorkOrder.Type type);
List<WorkOrder> findByStatus(WorkOrder.Status status); List<WorkOrder> findByStatusAndIsDeletedFalse(WorkOrder.Status status);
List<WorkOrder> findByAssignedTo(String assignedTo); List<WorkOrder> findByAssignedToAndIsDeletedFalse(String assignedTo);
@Query("SELECT w FROM WorkOrder w WHERE w.assignedDate < :date AND w.status IN ('PENDING', 'ASSIGNED')") @Query("SELECT w FROM WorkOrder w WHERE w.isDeleted = false AND w.assignedDate < :date AND w.status IN ('PENDING', 'ASSIGNED')")
List<WorkOrder> findOverdueTasks(@Param("date") LocalDate date); List<WorkOrder> findOverdueTasks(@Param("date") LocalDate date);
@Query("SELECT MAX(w.workNo) FROM WorkOrder w WHERE w.workNo LIKE :prefix") @Query("SELECT MAX(w.workNo) FROM WorkOrder w WHERE w.workNo LIKE :prefix")
String findMaxWorkNoByPrefix(@Param("prefix") String prefix); String findMaxWorkNoByPrefix(@Param("prefix") String prefix);
long countByStatus(WorkOrder.Status status); long countByStatusAndIsDeletedFalse(WorkOrder.Status status);
@Query("SELECT COUNT(w) FROM WorkOrder w WHERE w.status = 'COMPLETED' AND w.completedDate = :date") @Query("SELECT COUNT(w) FROM WorkOrder w WHERE w.isDeleted = false AND w.status = 'COMPLETED' AND w.completedDate = :date")
long countCompletedToday(@Param("date") LocalDate date); long countCompletedToday(@Param("date") LocalDate date);
@Query("SELECT COUNT(w) FROM WorkOrder w WHERE w.assignedDate < :date AND w.status IN ('PENDING', 'ASSIGNED')") @Query("SELECT COUNT(w) FROM WorkOrder w WHERE w.isDeleted = false AND w.assignedDate < :date AND w.status IN ('PENDING', 'ASSIGNED')")
long countOverdue(@Param("date") LocalDate date); long countOverdue(@Param("date") LocalDate date);
@Query("SELECT w.status, COUNT(w) FROM WorkOrder w GROUP BY w.status") @Query("SELECT w.status, COUNT(w) FROM WorkOrder w WHERE w.isDeleted = false GROUP BY w.status")
List<Object[]> countByStatus(); List<Object[]> countByStatus();
@Query("SELECT w.source, COUNT(w) FROM WorkOrder w GROUP BY w.source") @Query("SELECT w.source, COUNT(w) FROM WorkOrder w WHERE w.isDeleted = false GROUP BY w.source")
List<Object[]> countBySource(); List<Object[]> countBySource();
@Query("SELECT w.type, COUNT(w) FROM WorkOrder w GROUP BY w.type") @Query("SELECT w.type, COUNT(w) FROM WorkOrder w WHERE w.isDeleted = false GROUP BY w.type")
List<Object[]> countByType(); List<Object[]> countByType();
@Query("SELECT w.priority, COUNT(w) FROM WorkOrder w GROUP BY w.priority") @Query("SELECT w.priority, COUNT(w) FROM WorkOrder w WHERE w.isDeleted = false GROUP BY w.priority")
List<Object[]> countByPriority(); List<Object[]> countByPriority();
@Query("SELECT COUNT(w) FROM WorkOrder w WHERE w.createdAt >= :startDate AND w.createdAt < :endDate") @Query("SELECT COUNT(w) FROM WorkOrder w WHERE w.isDeleted = false AND w.createdAt >= :startDate AND w.createdAt < :endDate")
long countByCreatedAtBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); long countByCreatedAtBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
List<WorkOrder> findByPlanId(UUID planId); List<WorkOrder> findByPlanIdAndIsDeletedFalse(UUID planId);
List<WorkOrder> findByPlanIdAndCreatedAtBetween(UUID planId, LocalDateTime startDate, LocalDateTime endDate); List<WorkOrder> findByPlanIdAndCreatedAtBetweenAndIsDeletedFalse(UUID planId, LocalDateTime startDate, LocalDateTime endDate);
Page<WorkOrder> findByProjectIdAndIsDeletedFalse(UUID projectId, Pageable pageable);
Page<WorkOrder> findByEquipmentIdAndIsDeletedFalse(UUID equipmentId, Pageable pageable);
Page<WorkOrder> findBySourceAndIsDeletedFalse(WorkOrder.Source source, Pageable pageable);
Page<WorkOrder> findByTypeAndIsDeletedFalse(WorkOrder.Type type, Pageable pageable);
Page<WorkOrder> findByStatusAndIsDeletedFalse(WorkOrder.Status status, Pageable pageable);
Page<WorkOrder> findByAssignedToAndIsDeletedFalse(String assignedTo, Pageable pageable);
@Query("SELECT w FROM WorkOrder w WHERE w.isDeleted = false AND " +
"(:projectId IS NULL OR w.projectId = :projectId) " +
"AND (:equipmentId IS NULL OR w.equipmentId = :equipmentId) " +
"AND (:source IS NULL OR w.source = :source) " +
"AND (:type IS NULL OR w.type = :type) " +
"AND (:status IS NULL OR w.status = :status) " +
"AND (:priority IS NULL OR w.priority = :priority) " +
"AND (:assignedTo IS NULL OR w.assignedTo = :assignedTo) " +
"AND (:keyword IS NULL OR w.workNo LIKE %:keyword% " +
"OR w.title LIKE %:keyword%)")
Page<WorkOrder> searchWorkOrders(@Param("projectId") UUID projectId,
@Param("equipmentId") UUID equipmentId,
@Param("source") WorkOrder.Source source,
@Param("type") WorkOrder.Type type,
@Param("status") WorkOrder.Status status,
@Param("priority") WorkOrder.Priority priority,
@Param("assignedTo") String assignedTo,
@Param("keyword") String keyword,
Pageable pageable);
Optional<WorkOrder> findByIdAndIsDeletedFalse(UUID id);
List<WorkOrder> findAllByIsDeletedFalse();
} }

View File

@ -108,7 +108,7 @@ public class MaintenanceScheduler {
LocalDateTime todayStart = today.atStartOfDay(); LocalDateTime todayStart = today.atStartOfDay();
LocalDateTime tomorrowStart = today.plusDays(1).atStartOfDay(); LocalDateTime tomorrowStart = today.plusDays(1).atStartOfDay();
List<WorkOrder> todayTasks = workOrderRepository.findByPlanIdAndCreatedAtBetween( List<WorkOrder> todayTasks = workOrderRepository.findByPlanIdAndCreatedAtBetweenAndIsDeletedFalse(
planId, todayStart, tomorrowStart); planId, todayStart, tomorrowStart);
return !todayTasks.isEmpty(); return !todayTasks.isEmpty();

View File

@ -1,5 +1,6 @@
package com.ether.pms.ops.service; package com.ether.pms.ops.service;
import com.ether.pms.mdm.dto.PageResponse;
import com.ether.pms.ops.dto.WorkOrderStatsDTO; import com.ether.pms.ops.dto.WorkOrderStatsDTO;
import com.ether.pms.ops.entity.WorkOrder; import com.ether.pms.ops.entity.WorkOrder;
import com.ether.pms.ops.entity.WorkOrderItem; import com.ether.pms.ops.entity.WorkOrderItem;
@ -23,11 +24,19 @@ public interface WorkOrderService {
List<WorkOrder> getWorkOrdersByAssignedTo(String assignedTo); List<WorkOrder> getWorkOrdersByAssignedTo(String assignedTo);
List<WorkOrder> getOverdueWorkOrders(LocalDate date); List<WorkOrder> getOverdueWorkOrders(LocalDate date);
PageResponse<WorkOrder> queryWorkOrders(UUID projectId, UUID equipmentId, WorkOrder.Source source,
WorkOrder.Type type, WorkOrder.Status status,
WorkOrder.Priority priority, String assignedTo,
String keyword, int page, int size);
WorkOrder assignWorkOrder(UUID id, String assignedTo, String assignedVendor, LocalDate assignedDate); WorkOrder assignWorkOrder(UUID id, String assignedTo, String assignedVendor, LocalDate assignedDate);
WorkOrder startWorkOrder(UUID id); WorkOrder startWorkOrder(UUID id);
WorkOrder completeWorkOrder(UUID id, WorkOrder workOrderData); WorkOrder completeWorkOrder(UUID id, WorkOrder workOrderData);
WorkOrder verifyWorkOrder(UUID id, String verifiedBy, String remark, Integer rating); WorkOrder verifyWorkOrder(UUID id, String verifiedBy, String remark, Integer rating);
WorkOrder cancelWorkOrder(UUID id); WorkOrder cancelWorkOrder(UUID id);
WorkOrder suspendWorkOrder(UUID id);
WorkOrder resumeWorkOrder(UUID id);
WorkOrder returnWorkOrder(UUID id);
WorkOrderStatsDTO getWorkOrderStats(); WorkOrderStatsDTO getWorkOrderStats();

View File

@ -1,12 +1,18 @@
package com.ether.pms.ops.service.impl; package com.ether.pms.ops.service.impl;
import com.ether.pms.mdm.dto.PageResponse;
import com.ether.pms.ops.dto.WorkOrderStatsDTO; import com.ether.pms.ops.dto.WorkOrderStatsDTO;
import com.ether.pms.ops.entity.WorkOrder; import com.ether.pms.ops.entity.WorkOrder;
import com.ether.pms.ops.entity.WorkOrderItem; import com.ether.pms.ops.entity.WorkOrderItem;
import com.ether.pms.ops.repository.WorkOrderItemRepository; import com.ether.pms.ops.repository.WorkOrderItemRepository;
import com.ether.pms.ops.repository.WorkOrderRepository; import com.ether.pms.ops.repository.WorkOrderRepository;
import com.ether.pms.ops.service.WorkOrderService; import com.ether.pms.ops.service.WorkOrderService;
import com.ether.pms.common.util.PaginationValidator;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -71,12 +77,15 @@ public class WorkOrderServiceImpl implements WorkOrderService {
@Override @Override
@Transactional @Transactional
public void deleteWorkOrder(UUID id) { public void deleteWorkOrder(UUID id) {
workOrderRepository.deleteById(id); WorkOrder workOrder = workOrderRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow(() -> new RuntimeException("工单不存在: " + id));
workOrder.setIsDeleted(true);
workOrderRepository.save(workOrder);
} }
@Override @Override
public WorkOrder getWorkOrderById(UUID id) { public WorkOrder getWorkOrderById(UUID id) {
return workOrderRepository.findById(id) return workOrderRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow(() -> new RuntimeException("工单不存在: " + id)); .orElseThrow(() -> new RuntimeException("工单不存在: " + id));
} }
@ -86,37 +95,37 @@ public class WorkOrderServiceImpl implements WorkOrderService {
// 此方法用于管理后台的工单列表展示 // 此方法用于管理后台的工单列表展示
// TODO: 必须改为分页查询建议添加 Pageable 参数 // TODO: 必须改为分页查询建议添加 Pageable 参数
// 临时方案限制返回最近 1000 条记录 // 临时方案限制返回最近 1000 条记录
return workOrderRepository.findAll(); return workOrderRepository.findAllByIsDeletedFalse();
} }
@Override @Override
public List<WorkOrder> getWorkOrdersByProject(UUID projectId) { public List<WorkOrder> getWorkOrdersByProject(UUID projectId) {
return workOrderRepository.findByProjectId(projectId); return workOrderRepository.findByProjectIdAndIsDeletedFalse(projectId);
} }
@Override @Override
public List<WorkOrder> getWorkOrdersByEquipment(UUID equipmentId) { public List<WorkOrder> getWorkOrdersByEquipment(UUID equipmentId) {
return workOrderRepository.findByEquipmentId(equipmentId); return workOrderRepository.findByEquipmentIdAndIsDeletedFalse(equipmentId);
} }
@Override @Override
public List<WorkOrder> getWorkOrdersBySource(WorkOrder.Source source) { public List<WorkOrder> getWorkOrdersBySource(WorkOrder.Source source) {
return workOrderRepository.findBySource(source); return workOrderRepository.findBySourceAndIsDeletedFalse(source);
} }
@Override @Override
public List<WorkOrder> getWorkOrdersByType(WorkOrder.Type type) { public List<WorkOrder> getWorkOrdersByType(WorkOrder.Type type) {
return workOrderRepository.findByType(type); return workOrderRepository.findByTypeAndIsDeletedFalse(type);
} }
@Override @Override
public List<WorkOrder> getWorkOrdersByStatus(WorkOrder.Status status) { public List<WorkOrder> getWorkOrdersByStatus(WorkOrder.Status status) {
return workOrderRepository.findByStatus(status); return workOrderRepository.findByStatusAndIsDeletedFalse(status);
} }
@Override @Override
public List<WorkOrder> getWorkOrdersByAssignedTo(String assignedTo) { public List<WorkOrder> getWorkOrdersByAssignedTo(String assignedTo) {
return workOrderRepository.findByAssignedTo(assignedTo); return workOrderRepository.findByAssignedToAndIsDeletedFalse(assignedTo);
} }
@Override @Override
@ -124,6 +133,24 @@ public class WorkOrderServiceImpl implements WorkOrderService {
return workOrderRepository.findOverdueTasks(date); return workOrderRepository.findOverdueTasks(date);
} }
@Override
public PageResponse<WorkOrder> queryWorkOrders(UUID projectId, UUID equipmentId, WorkOrder.Source source,
WorkOrder.Type type, WorkOrder.Status status,
WorkOrder.Priority priority, String assignedTo,
String keyword, int page, int size) {
int safePage = PaginationValidator.getSafePage(page);
int safeSize = PaginationValidator.getSafeSize(size);
Pageable pageable = PageRequest.of(safePage, safeSize, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<WorkOrder> workOrderPage = workOrderRepository.searchWorkOrders(
projectId, equipmentId, source, type, status, priority, assignedTo, keyword, pageable);
return PageResponse.of(
workOrderPage.getContent(),
workOrderPage.getNumber(),
workOrderPage.getSize(),
workOrderPage.getTotalElements()
);
}
@Override @Override
@Transactional @Transactional
public WorkOrder assignWorkOrder(UUID id, String assignedTo, String assignedVendor, LocalDate assignedDate) { public WorkOrder assignWorkOrder(UUID id, String assignedTo, String assignedVendor, LocalDate assignedDate) {
@ -226,6 +253,55 @@ public class WorkOrderServiceImpl implements WorkOrderService {
return workOrderRepository.save(workOrder); return workOrderRepository.save(workOrder);
} }
@Override
@Transactional
public WorkOrder suspendWorkOrder(UUID id) {
WorkOrder workOrder = getWorkOrderById(id);
if (workOrder.getStatus() != WorkOrder.Status.ASSIGNED &&
workOrder.getStatus() != WorkOrder.Status.IN_PROGRESS) {
throw new RuntimeException("只能挂起已派单或执行中的工单");
}
workOrder.setPreviousStatus(workOrder.getStatus());
workOrder.setStatus(WorkOrder.Status.SUSPENDED);
return workOrderRepository.save(workOrder);
}
@Override
@Transactional
public WorkOrder resumeWorkOrder(UUID id) {
WorkOrder workOrder = getWorkOrderById(id);
if (workOrder.getStatus() != WorkOrder.Status.SUSPENDED) {
throw new RuntimeException("只能恢复已挂起的工单");
}
WorkOrder.Status targetStatus = workOrder.getPreviousStatus() != null
? workOrder.getPreviousStatus()
: WorkOrder.Status.IN_PROGRESS;
workOrder.setPreviousStatus(null);
workOrder.setStatus(targetStatus);
return workOrderRepository.save(workOrder);
}
@Override
@Transactional
public WorkOrder returnWorkOrder(UUID id) {
WorkOrder workOrder = getWorkOrderById(id);
if (workOrder.getStatus() != WorkOrder.Status.ASSIGNED) {
throw new RuntimeException("只能退回已派单的工单");
}
workOrder.setPreviousStatus(null);
workOrder.setAssignedTo(null);
workOrder.setAssignedVendor(null);
workOrder.setAssignedDate(null);
workOrder.setStatus(WorkOrder.Status.PENDING);
return workOrderRepository.save(workOrder);
}
@Override @Override
public WorkOrderStatsDTO getWorkOrderStats() { public WorkOrderStatsDTO getWorkOrderStats() {
LocalDate today = LocalDate.now(); LocalDate today = LocalDate.now();
@ -278,6 +354,8 @@ public class WorkOrderServiceImpl implements WorkOrderService {
.completed(byStatus.getOrDefault("COMPLETED", 0L)) .completed(byStatus.getOrDefault("COMPLETED", 0L))
.verified(byStatus.getOrDefault("VERIFIED", 0L)) .verified(byStatus.getOrDefault("VERIFIED", 0L))
.cancelled(byStatus.getOrDefault("CANCELLED", 0L)) .cancelled(byStatus.getOrDefault("CANCELLED", 0L))
.suspended(byStatus.getOrDefault("SUSPENDED", 0L))
.returned(byStatus.getOrDefault("RETURNED", 0L))
.completedToday(workOrderRepository.countCompletedToday(today)) .completedToday(workOrderRepository.countCompletedToday(today))
.createdToday(workOrderRepository.countByCreatedAtBetween(startOfToday, endOfToday)) .createdToday(workOrderRepository.countByCreatedAtBetween(startOfToday, endOfToday))
.overdue(workOrderRepository.countOverdue(today)) .overdue(workOrderRepository.countOverdue(today))

View File

@ -123,7 +123,7 @@ class WorkOrderServiceTest {
@Test @Test
@DisplayName("派单PENDING -> ASSIGNED") @DisplayName("派单PENDING -> ASSIGNED")
void assignWorkOrder_shouldChangeStatusToAssigned() { void assignWorkOrder_shouldChangeStatusToAssigned() {
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0));
WorkOrder result = workOrderService.assignWorkOrder(testId, "张三", "维保公司A", LocalDate.now()); WorkOrder result = workOrderService.assignWorkOrder(testId, "张三", "维保公司A", LocalDate.now());
@ -138,7 +138,7 @@ class WorkOrderServiceTest {
@DisplayName("派单失败只有PENDING状态才能派单") @DisplayName("派单失败只有PENDING状态才能派单")
void assignWorkOrder_shouldFailWhenNotPending() { void assignWorkOrder_shouldFailWhenNotPending() {
testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED); testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED);
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
assertThrows(RuntimeException.class, () -> assertThrows(RuntimeException.class, () ->
workOrderService.assignWorkOrder(testId, "张三", "维保公司A", LocalDate.now()) workOrderService.assignWorkOrder(testId, "张三", "维保公司A", LocalDate.now())
@ -149,7 +149,7 @@ class WorkOrderServiceTest {
@DisplayName("开始ASSIGNED -> IN_PROGRESS") @DisplayName("开始ASSIGNED -> IN_PROGRESS")
void startWorkOrder_shouldChangeStatusToInProgress() { void startWorkOrder_shouldChangeStatusToInProgress() {
testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED); testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED);
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0));
WorkOrder result = workOrderService.startWorkOrder(testId); WorkOrder result = workOrderService.startWorkOrder(testId);
@ -162,7 +162,7 @@ class WorkOrderServiceTest {
@DisplayName("开始失败只有ASSIGNED状态才能开始") @DisplayName("开始失败只有ASSIGNED状态才能开始")
void startWorkOrder_shouldFailWhenNotAssigned() { void startWorkOrder_shouldFailWhenNotAssigned() {
testWorkOrder.setStatus(WorkOrder.Status.PENDING); testWorkOrder.setStatus(WorkOrder.Status.PENDING);
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
assertThrows(RuntimeException.class, () -> assertThrows(RuntimeException.class, () ->
workOrderService.startWorkOrder(testId) workOrderService.startWorkOrder(testId)
@ -184,7 +184,7 @@ class WorkOrderServiceTest {
completeData.setTotalCost(BigDecimal.valueOf(700)); completeData.setTotalCost(BigDecimal.valueOf(700));
completeData.setCompletedBy("李四"); completeData.setCompletedBy("李四");
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0));
WorkOrder result = workOrderService.completeWorkOrder(testId, completeData); WorkOrder result = workOrderService.completeWorkOrder(testId, completeData);
@ -202,7 +202,7 @@ class WorkOrderServiceTest {
@DisplayName("完成失败只有IN_PROGRESS状态才能完成") @DisplayName("完成失败只有IN_PROGRESS状态才能完成")
void completeWorkOrder_shouldFailWhenNotInProgress() { void completeWorkOrder_shouldFailWhenNotInProgress() {
testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED); testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED);
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
assertThrows(RuntimeException.class, () -> assertThrows(RuntimeException.class, () ->
workOrderService.completeWorkOrder(testId, new WorkOrder()) workOrderService.completeWorkOrder(testId, new WorkOrder())
@ -213,7 +213,7 @@ class WorkOrderServiceTest {
@DisplayName("验收COMPLETED -> VERIFIED") @DisplayName("验收COMPLETED -> VERIFIED")
void verifyWorkOrder_shouldChangeStatusToVerified() { void verifyWorkOrder_shouldChangeStatusToVerified() {
testWorkOrder.setStatus(WorkOrder.Status.COMPLETED); testWorkOrder.setStatus(WorkOrder.Status.COMPLETED);
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0));
WorkOrder result = workOrderService.verifyWorkOrder(testId, "王五", "验收通过", 5); WorkOrder result = workOrderService.verifyWorkOrder(testId, "王五", "验收通过", 5);
@ -229,7 +229,7 @@ class WorkOrderServiceTest {
@DisplayName("验收失败只有COMPLETED状态才能验收") @DisplayName("验收失败只有COMPLETED状态才能验收")
void verifyWorkOrder_shouldFailWhenNotCompleted() { void verifyWorkOrder_shouldFailWhenNotCompleted() {
testWorkOrder.setStatus(WorkOrder.Status.IN_PROGRESS); testWorkOrder.setStatus(WorkOrder.Status.IN_PROGRESS);
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
assertThrows(RuntimeException.class, () -> assertThrows(RuntimeException.class, () ->
workOrderService.verifyWorkOrder(testId, "王五", "验收通过", 5) workOrderService.verifyWorkOrder(testId, "王五", "验收通过", 5)
@ -240,7 +240,7 @@ class WorkOrderServiceTest {
@DisplayName("验收评分应在1-5范围内") @DisplayName("验收评分应在1-5范围内")
void verifyWorkOrder_shouldAcceptValidRating() { void verifyWorkOrder_shouldAcceptValidRating() {
testWorkOrder.setStatus(WorkOrder.Status.COMPLETED); testWorkOrder.setStatus(WorkOrder.Status.COMPLETED);
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0));
WorkOrder result = workOrderService.verifyWorkOrder(testId, "王五", null, 4); WorkOrder result = workOrderService.verifyWorkOrder(testId, "王五", null, 4);
@ -252,7 +252,7 @@ class WorkOrderServiceTest {
@DisplayName("取消PENDING/ASSIGNED/IN_PROGRESS -> CANCELLED") @DisplayName("取消PENDING/ASSIGNED/IN_PROGRESS -> CANCELLED")
void cancelWorkOrder_shouldChangeStatusToCancelled() { void cancelWorkOrder_shouldChangeStatusToCancelled() {
testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED); testWorkOrder.setStatus(WorkOrder.Status.ASSIGNED);
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0));
WorkOrder result = workOrderService.cancelWorkOrder(testId); WorkOrder result = workOrderService.cancelWorkOrder(testId);
@ -264,7 +264,7 @@ class WorkOrderServiceTest {
@DisplayName("取消失败COMPLETED状态不能取消") @DisplayName("取消失败COMPLETED状态不能取消")
void cancelWorkOrder_shouldFailWhenCompleted() { void cancelWorkOrder_shouldFailWhenCompleted() {
testWorkOrder.setStatus(WorkOrder.Status.COMPLETED); testWorkOrder.setStatus(WorkOrder.Status.COMPLETED);
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
assertThrows(RuntimeException.class, () -> assertThrows(RuntimeException.class, () ->
workOrderService.cancelWorkOrder(testId) workOrderService.cancelWorkOrder(testId)
@ -275,7 +275,7 @@ class WorkOrderServiceTest {
@DisplayName("取消失败VERIFIED状态不能取消") @DisplayName("取消失败VERIFIED状态不能取消")
void cancelWorkOrder_shouldFailWhenVerified() { void cancelWorkOrder_shouldFailWhenVerified() {
testWorkOrder.setStatus(WorkOrder.Status.VERIFIED); testWorkOrder.setStatus(WorkOrder.Status.VERIFIED);
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
assertThrows(RuntimeException.class, () -> assertThrows(RuntimeException.class, () ->
workOrderService.cancelWorkOrder(testId) workOrderService.cancelWorkOrder(testId)
@ -290,7 +290,7 @@ class WorkOrderServiceTest {
@Test @Test
@DisplayName("根据ID获取工单") @DisplayName("根据ID获取工单")
void getWorkOrderById_shouldReturnWorkOrder() { void getWorkOrderById_shouldReturnWorkOrder() {
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
WorkOrder result = workOrderService.getWorkOrderById(testId); WorkOrder result = workOrderService.getWorkOrderById(testId);
@ -301,7 +301,7 @@ class WorkOrderServiceTest {
@Test @Test
@DisplayName("根据ID获取工单失败时应抛出异常") @DisplayName("根据ID获取工单失败时应抛出异常")
void getWorkOrderById_shouldThrowExceptionWhenNotFound() { void getWorkOrderById_shouldThrowExceptionWhenNotFound() {
when(workOrderRepository.findById(testId)).thenReturn(Optional.empty()); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.empty());
assertThrows(RuntimeException.class, () -> assertThrows(RuntimeException.class, () ->
workOrderService.getWorkOrderById(testId) workOrderService.getWorkOrderById(testId)
@ -314,11 +314,15 @@ class WorkOrderServiceTest {
class DeleteTests { class DeleteTests {
@Test @Test
@DisplayName("删除工单应调用repository") @DisplayName("逻辑删除工单应设置isDeleted为true")
void deleteWorkOrder_shouldCallRepository() { void deleteWorkOrder_shouldSetIsDeletedTrue() {
when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0));
workOrderService.deleteWorkOrder(testId); workOrderService.deleteWorkOrder(testId);
verify(workOrderRepository).deleteById(testId); assertTrue(testWorkOrder.getIsDeleted());
verify(workOrderRepository).save(testWorkOrder);
} }
} }
@ -329,7 +333,7 @@ class WorkOrderServiceTest {
@Test @Test
@DisplayName("更新工单应保存所有字段") @DisplayName("更新工单应保存所有字段")
void updateWorkOrder_shouldUpdateAllFields() { void updateWorkOrder_shouldUpdateAllFields() {
when(workOrderRepository.findById(testId)).thenReturn(Optional.of(testWorkOrder)); when(workOrderRepository.findByIdAndIsDeletedFalse(testId)).thenReturn(Optional.of(testWorkOrder));
when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0)); when(workOrderRepository.save(any(WorkOrder.class))).thenAnswer(invocation -> invocation.getArgument(0));
WorkOrder updateData = new WorkOrder(); WorkOrder updateData = new WorkOrder();