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}")
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")
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")
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")

View File

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

View File

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

View File

@ -21,6 +21,7 @@ public class DataAccessController {
private final DataAccessService dataAccessService;
@Deprecated
@PostMapping
public ResponseEntity<ApiResponse<Void>> grantAccess(@Valid @RequestBody DataAccessRequest request) {
UUID currentUserId = SecurityUtils.getCurrentUserId();
@ -38,12 +39,14 @@ public class DataAccessController {
return ResponseEntity.ok(ApiResponse.success());
}
@Deprecated
@DeleteMapping("/{id}")
public ResponseEntity<ApiResponse<Void>> revokeAccess(@PathVariable UUID id) {
dataAccessService.revokeAccess(id);
return ResponseEntity.ok(ApiResponse.success());
}
@Deprecated
@GetMapping
public ResponseEntity<ApiResponse<List<DataAccess>>> getDataAccess(
@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.common.ApiResponse;
import jakarta.validation.Valid;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
@ -87,7 +88,7 @@ public class RoleController {
* @return 包含该项目角色列表的响应
*/
@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)));
}

View File

@ -8,6 +8,7 @@ import java.util.UUID;
@Entity
@Table(name = "biz_data_access")
@Data
@Deprecated
public class DataAccess {
@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;
@Column(nullable = false)
private Boolean visible = true;
/**
* 权限关联的角色列表
* 多对多关系通过auth_role_permission关联表维护

View File

@ -33,14 +33,14 @@ class PasswordServiceTest {
}
@Test
void isPasswordWeak_shouldReturnTrue_forCommonWeakPasswords() {
assertTrue(passwordService.isPasswordWeak("password123"));
assertTrue(passwordService.isPasswordWeak("admin123"));
assertTrue(passwordService.isPasswordWeak("qwerty123"));
void isWeakPassword_shouldReturnTrue_forCommonWeakPasswords() {
assertTrue(passwordService.isWeakPassword("password123"));
assertTrue(passwordService.isWeakPassword("admin123"));
assertTrue(passwordService.isWeakPassword("qwerty123"));
}
@Test
void isPasswordWeak_shouldReturnFalse_forStrongPasswords() {
assertFalse(passwordService.isPasswordWeak("Str0ng!Pass"));
void isWeakPassword_shouldReturnFalse_forStrongPasswords() {
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.UserRepository;
import com.ether.pms.common.BusinessException;
import com.ether.pms.common.ErrorCode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@ -86,8 +87,8 @@ class UserServiceTest {
newUser.setPassword("weak");
when(userRepository.existsByUsername("newuser")).thenReturn(false);
doThrow(new IllegalArgumentException("密码太弱"))
.when(passwordService).validatePassword("weak");
doThrow(new BusinessException(ErrorCode.PASSWORD_001))
.when(passwordService).validateStrength("weak");
assertThrows(BusinessException.class, () -> userService.create(newUser));
}
@ -99,8 +100,8 @@ class UserServiceTest {
newUser.setPassword("Valid123!");
when(userRepository.existsByUsername("newuser")).thenReturn(false);
doNothing().when(passwordService).validatePassword("Valid123!");
when(passwordService.isPasswordWeak("Valid123!")).thenReturn(false);
doNothing().when(passwordService).validateStrength("Valid123!");
when(passwordService.isWeakPassword("Valid123!")).thenReturn(false);
when(passwordService.encode("Valid123!")).thenReturn("encodedPassword");
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")
public ResponseEntity<ApiResponse<SpaceNodeEquipmentDTO>> getEquipment(@PathVariable UUID id) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.getEquipmentById(id)));
@ -133,7 +135,9 @@ public class SpaceNodeController {
/**
* 获取设备列表
* @deprecated 请使用 /api/asset/equipment 替代
*/
@Deprecated
@GetMapping("/equipment")
public ResponseEntity<ApiResponse<List<SpaceNodeEquipmentDTO>>> getEquipmentList(
@RequestParam UUID projectId) {
@ -142,7 +146,9 @@ public class SpaceNodeController {
/**
* 获取特种设备列表
* @deprecated 请使用 /api/asset/equipment 替代
*/
@Deprecated
@GetMapping("/special-equipment")
public ResponseEntity<ApiResponse<List<SpaceNodeEquipmentDTO>>> getSpecialEquipment(
@RequestParam UUID projectId) {
@ -151,7 +157,9 @@ public class SpaceNodeController {
/**
* 获取即将年检设备
* @deprecated 请使用 /api/asset/equipment 替代
*/
@Deprecated
@GetMapping("/expiring-inspection")
public ResponseEntity<ApiResponse<List<SpaceNodeEquipmentDTO>>> getExpiringInspection(
@RequestParam UUID projectId,
@ -161,7 +169,9 @@ public class SpaceNodeController {
/**
* 创建设备
* @deprecated 请使用 /api/asset/equipment 替代
*/
@Deprecated
@PostMapping("/equipment")
public ResponseEntity<ApiResponse<SpaceNode>> createEquipment(@Valid @RequestBody EquipmentCreateDTO dto) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.createEquipment(dto)));
@ -169,7 +179,9 @@ public class SpaceNodeController {
/**
* 批量创建设备
* @deprecated 请使用 /api/asset/equipment 替代
*/
@Deprecated
@PostMapping("/equipment/batch")
public ResponseEntity<ApiResponse<List<SpaceNode>>> batchCreateEquipment(@Valid @RequestBody List<EquipmentCreateDTO> dtoList) {
return ResponseEntity.ok(ApiResponse.success(spaceNodeService.batchCreateEquipment(dtoList)));
@ -177,7 +189,9 @@ public class SpaceNodeController {
/**
* Excel导入设备
* @deprecated 请使用 /api/asset/equipment 替代
*/
@Deprecated
@PostMapping("/equipment/import")
public ResponseEntity<ApiResponse<Object>> importEquipment(
@RequestParam("file") MultipartFile file,

View File

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

View File

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

View File

@ -65,4 +65,7 @@ public interface SpaceNodeRepository extends JpaRepository<SpaceNode, UUID> {
* 统计项目下指定类型空间数量
*/
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.FloorDetailVO;
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.ProjectRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -27,6 +29,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@ -40,6 +43,7 @@ import com.ether.pms.mdm.dto.EquipmentCreateDTO;
public class SpaceNodeService {
private final SpaceNodeRepository spaceNodeRepository;
private final ProjectRepository projectRepository;
private final ObjectMapper objectMapper;
/**
@ -142,6 +146,13 @@ public class SpaceNodeService {
node.setNodeCategory(nodeType.getCategory());
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.setSortOrder(dto.getSortOrder() != null ? dto.getSortOrder() : 0);
node.setStatus(dto.getStatus() != null ? dto.getStatus() : "ACTIVE");
@ -734,6 +745,46 @@ public class SpaceNodeService {
/**
* 楼栋楼层配置从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
public static class BuildingFloorConfig {
private Integer totalFloors;

View File

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

View File

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

View File

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

View File

@ -57,9 +57,13 @@ public class WorkOrder {
private Status status = Status.PENDING;
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)
private String title;
@ -147,6 +151,9 @@ public class WorkOrder {
@Column(length = 2000)
private String signature;
@Column(name = "is_deleted")
private Boolean isDeleted = false;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@ -160,6 +167,9 @@ public class WorkOrder {
public void prePersist() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (isDeleted == null) {
isDeleted = false;
}
}
@PreUpdate

View File

@ -1,7 +1,10 @@
package com.ether.pms.ops.repository;
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.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@ -13,51 +16,87 @@ import java.util.Optional;
import java.util.UUID;
@Repository
public interface WorkOrderRepository extends JpaRepository<WorkOrder, UUID> {
Optional<WorkOrder> findByWorkNo(String workNo);
public interface WorkOrderRepository extends JpaRepository<WorkOrder, UUID>, JpaSpecificationExecutor<WorkOrder> {
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);
@Query("SELECT MAX(w.workNo) FROM WorkOrder w WHERE w.workNo LIKE :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);
@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);
@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();
@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();
@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();
@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();
@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);
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 tomorrowStart = today.plusDays(1).atStartOfDay();
List<WorkOrder> todayTasks = workOrderRepository.findByPlanIdAndCreatedAtBetween(
List<WorkOrder> todayTasks = workOrderRepository.findByPlanIdAndCreatedAtBetweenAndIsDeletedFalse(
planId, todayStart, tomorrowStart);
return !todayTasks.isEmpty();

View File

@ -1,5 +1,6 @@
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.entity.WorkOrder;
import com.ether.pms.ops.entity.WorkOrderItem;
@ -23,11 +24,19 @@ public interface WorkOrderService {
List<WorkOrder> getWorkOrdersByAssignedTo(String assignedTo);
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 startWorkOrder(UUID id);
WorkOrder completeWorkOrder(UUID id, WorkOrder workOrderData);
WorkOrder verifyWorkOrder(UUID id, String verifiedBy, String remark, Integer rating);
WorkOrder cancelWorkOrder(UUID id);
WorkOrder suspendWorkOrder(UUID id);
WorkOrder resumeWorkOrder(UUID id);
WorkOrder returnWorkOrder(UUID id);
WorkOrderStatsDTO getWorkOrderStats();

View File

@ -1,12 +1,18 @@
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.entity.WorkOrder;
import com.ether.pms.ops.entity.WorkOrderItem;
import com.ether.pms.ops.repository.WorkOrderItemRepository;
import com.ether.pms.ops.repository.WorkOrderRepository;
import com.ether.pms.ops.service.WorkOrderService;
import com.ether.pms.common.util.PaginationValidator;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -71,12 +77,15 @@ public class WorkOrderServiceImpl implements WorkOrderService {
@Override
@Transactional
public void deleteWorkOrder(UUID id) {
workOrderRepository.deleteById(id);
WorkOrder workOrder = workOrderRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow(() -> new RuntimeException("工单不存在: " + id));
workOrder.setIsDeleted(true);
workOrderRepository.save(workOrder);
}
@Override
public WorkOrder getWorkOrderById(UUID id) {
return workOrderRepository.findById(id)
return workOrderRepository.findByIdAndIsDeletedFalse(id)
.orElseThrow(() -> new RuntimeException("工单不存在: " + id));
}
@ -86,37 +95,37 @@ public class WorkOrderServiceImpl implements WorkOrderService {
// 此方法用于管理后台的工单列表展示
// TODO: 必须改为分页查询建议添加 Pageable 参数
// 临时方案限制返回最近 1000 条记录
return workOrderRepository.findAll();
return workOrderRepository.findAllByIsDeletedFalse();
}
@Override
public List<WorkOrder> getWorkOrdersByProject(UUID projectId) {
return workOrderRepository.findByProjectId(projectId);
return workOrderRepository.findByProjectIdAndIsDeletedFalse(projectId);
}
@Override
public List<WorkOrder> getWorkOrdersByEquipment(UUID equipmentId) {
return workOrderRepository.findByEquipmentId(equipmentId);
return workOrderRepository.findByEquipmentIdAndIsDeletedFalse(equipmentId);
}
@Override
public List<WorkOrder> getWorkOrdersBySource(WorkOrder.Source source) {
return workOrderRepository.findBySource(source);
return workOrderRepository.findBySourceAndIsDeletedFalse(source);
}
@Override
public List<WorkOrder> getWorkOrdersByType(WorkOrder.Type type) {
return workOrderRepository.findByType(type);
return workOrderRepository.findByTypeAndIsDeletedFalse(type);
}
@Override
public List<WorkOrder> getWorkOrdersByStatus(WorkOrder.Status status) {
return workOrderRepository.findByStatus(status);
return workOrderRepository.findByStatusAndIsDeletedFalse(status);
}
@Override
public List<WorkOrder> getWorkOrdersByAssignedTo(String assignedTo) {
return workOrderRepository.findByAssignedTo(assignedTo);
return workOrderRepository.findByAssignedToAndIsDeletedFalse(assignedTo);
}
@Override
@ -124,6 +133,24 @@ public class WorkOrderServiceImpl implements WorkOrderService {
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
@Transactional
public WorkOrder assignWorkOrder(UUID id, String assignedTo, String assignedVendor, LocalDate assignedDate) {
@ -226,6 +253,55 @@ public class WorkOrderServiceImpl implements WorkOrderService {
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
public WorkOrderStatsDTO getWorkOrderStats() {
LocalDate today = LocalDate.now();
@ -278,6 +354,8 @@ public class WorkOrderServiceImpl implements WorkOrderService {
.completed(byStatus.getOrDefault("COMPLETED", 0L))
.verified(byStatus.getOrDefault("VERIFIED", 0L))
.cancelled(byStatus.getOrDefault("CANCELLED", 0L))
.suspended(byStatus.getOrDefault("SUSPENDED", 0L))
.returned(byStatus.getOrDefault("RETURNED", 0L))
.completedToday(workOrderRepository.countCompletedToday(today))
.createdToday(workOrderRepository.countByCreatedAtBetween(startOfToday, endOfToday))
.overdue(workOrderRepository.countOverdue(today))

View File

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