From eb1ddfcaa8ffe25ee230ca6f1676e47b7f45182f Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Wed, 23 Sep 2026 15:29:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BD=8F=E5=AE=BF=EF=BC=9A?= =?UTF-8?q?=E5=BA=8A=E4=BD=8D=E9=87=8D=E5=8F=A0=E5=86=B2=E7=AA=81=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E6=A0=87=E5=87=86=E5=8C=96=E4=B8=9A=E5=8A=A1=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ationAndAuthApiControllerContractTest.java | 26 ++++++++++++++++++ .../constant/AccommodationErrorCode.java | 27 +++++++++++++++++++ .../impl/TrainAccommodationServiceImpl.java | 13 ++++++--- .../service/impl/BedOccupancyModelCTest.java | 4 +++ 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/constant/AccommodationErrorCode.java diff --git a/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/AccommodationAndAuthApiControllerContractTest.java b/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/AccommodationAndAuthApiControllerContractTest.java index 2305a5f98..7daf7321c 100644 --- a/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/AccommodationAndAuthApiControllerContractTest.java +++ b/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/train/AccommodationAndAuthApiControllerContractTest.java @@ -2,6 +2,7 @@ package com.ruoyi.web.controller.train; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.lang.reflect.Method; @@ -181,5 +182,30 @@ public class AccommodationAndAuthApiControllerContractTest (java.util.Map) controller.getCapabilities().get("data"); assertEquals(Boolean.FALSE, missing.get("manualRevokeAvailable")); } + + /** + * EX-13: 床位时间窗口冲突必须返回标准业务错误码 (AccommodationErrorCode.BED_ALREADY_OCCUPIED = 40060), + * 严禁退化为未处理的系统 500 异常。 + */ + @Test + public void testEx13_BedOverlapErrorResponseContract() + { + com.ruoyi.framework.web.exception.GlobalExceptionHandler handler = + new com.ruoyi.framework.web.exception.GlobalExceptionHandler(); + + com.ruoyi.common.exception.ServiceException ex = new com.ruoyi.common.exception.ServiceException( + "BED_ALREADY_OCCUPIED: 该床位在目标培训周期内已被占用 (宿舍: DORM_101,床位: 1号床)", + com.ruoyi.train.constant.AccommodationErrorCode.BED_ALREADY_OCCUPIED); + + com.ruoyi.common.core.domain.AjaxResult result = handler.handleServiceException(ex, null); + assertNotNull(result); + assertEquals("HTTP 响应体 code 必须是标准业务错误码 40060", + Integer.valueOf(com.ruoyi.train.constant.AccommodationErrorCode.BED_ALREADY_OCCUPIED), + result.get(com.ruoyi.common.core.domain.AjaxResult.CODE_TAG)); + assertTrue("响应体 code 严禁为系统 500 异常", + !Integer.valueOf(500).equals(result.get(com.ruoyi.common.core.domain.AjaxResult.CODE_TAG))); + assertTrue("响应体消息必须包含 BED_ALREADY_OCCUPIED 提示", + String.valueOf(result.get(com.ruoyi.common.core.domain.AjaxResult.MSG_TAG)).contains("BED_ALREADY_OCCUPIED")); + } } diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/constant/AccommodationErrorCode.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/constant/AccommodationErrorCode.java new file mode 100644 index 000000000..7faaa6ad9 --- /dev/null +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/constant/AccommodationErrorCode.java @@ -0,0 +1,27 @@ +package com.ruoyi.train.constant; + +/** + * 住宿管理业务错误码与常量规范 (API Contract V1.0.1 Module D)。 + */ +public final class AccommodationErrorCode +{ + /** 床位时间窗口已被占用冲突 */ + public static final int BED_ALREADY_OCCUPIED = 40060; + public static final String BED_ALREADY_OCCUPIED_MESSAGE = "该床位在目标培训周期内已被占用"; + + /** 开班计划时间窗口不存在或无效 */ + public static final int BED_OCCUPANCY_WINDOW_INVALID = 40061; + public static final String BED_OCCUPANCY_WINDOW_INVALID_MESSAGE = "目标开班计划时间窗口不存在或无效"; + + /** 床位存在无法解析时间窗口的预订记录 */ + public static final int BED_OCCUPANCY_WINDOW_UNRESOLVED = 40062; + public static final String BED_OCCUPANCY_WINDOW_UNRESOLVED_MESSAGE = "目标床位存在无法解析培训时间窗口的有效预订记录"; + + /** 住宿记录不存在 */ + public static final int ACCOMMODATION_NOT_FOUND = 40460; + public static final String ACCOMMODATION_NOT_FOUND_MESSAGE = "住宿记录不存在"; + + private AccommodationErrorCode() + { + } +} diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainAccommodationServiceImpl.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainAccommodationServiceImpl.java index 7044520c5..53d628e08 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainAccommodationServiceImpl.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/service/impl/TrainAccommodationServiceImpl.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.train.constant.AccommodationErrorCode; import com.ruoyi.train.domain.TrainAccommodation; import com.ruoyi.train.domain.TrainClassPlan; import com.ruoyi.train.domain.TrainDormitoryInfo; @@ -462,14 +463,16 @@ public class TrainAccommodationServiceImpl implements ITrainAccommodationService TrainEnrollment enrollment = trainEnrollmentMapper.selectTrainEnrollmentById(enrollmentId); if (enrollment == null || enrollment.getPlanId() == null || enrollment.getPlanId().trim().isEmpty()) { - throw new ServiceException("BED_OCCUPANCY_WINDOW_INVALID: 无法解析参训档案关联的开班计划 (enrollmentId: " + enrollmentId + ")"); + throw new ServiceException("BED_OCCUPANCY_WINDOW_INVALID: 无法解析参训档案关联的开班计划 (enrollmentId: " + enrollmentId + ")", + AccommodationErrorCode.BED_OCCUPANCY_WINDOW_INVALID); } TrainClassPlan targetPlan = trainClassPlanMapper.selectTrainClassPlanByPlanId(enrollment.getPlanId()); if (targetPlan == null || targetPlan.getStartTime() == null || targetPlan.getEndTime() == null || !targetPlan.getEndTime().after(targetPlan.getStartTime())) { - throw new ServiceException("BED_OCCUPANCY_WINDOW_INVALID: 目标开班计划时间窗口不存在或无效 (planId: " + enrollment.getPlanId() + ")"); + throw new ServiceException("BED_OCCUPANCY_WINDOW_INVALID: 目标开班计划时间窗口不存在或无效 (planId: " + enrollment.getPlanId() + ")", + AccommodationErrorCode.BED_OCCUPANCY_WINDOW_INVALID); } // 2. 检查同一床位是否存在无法可靠解析时间窗口的已有 active accommodation (Fail Closed) @@ -477,7 +480,8 @@ public class TrainAccommodationServiceImpl implements ITrainAccommodationService if (unresolvedCount > 0) { throw new ServiceException("BED_OCCUPANCY_WINDOW_UNRESOLVED: 目标床位存在无法解析培训时间窗口的有效预订记录,请人工排查 (宿舍: " - + dormId + ",床位: " + bedNo + ")"); + + dormId + ",床位: " + bedNo + ")", + AccommodationErrorCode.BED_OCCUPANCY_WINDOW_UNRESOLVED); } // 3. 检查同一床位在目标时间区间 [start_time, end_time) 内是否存在正长度交集冲突 @@ -485,7 +489,8 @@ public class TrainAccommodationServiceImpl implements ITrainAccommodationService dormId, bedNo.trim(), targetPlan.getStartTime(), targetPlan.getEndTime(), excludeAccommodationId); if (overlapCount > 0) { - throw new ServiceException("BED_ALREADY_OCCUPIED: 该床位在目标培训周期内已被占用 (宿舍: " + dormId + ",床位: " + bedNo + ")"); + throw new ServiceException("BED_ALREADY_OCCUPIED: 该床位在目标培训周期内已被占用 (宿舍: " + dormId + ",床位: " + bedNo + ")", + AccommodationErrorCode.BED_ALREADY_OCCUPIED); } } } diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/BedOccupancyModelCTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/BedOccupancyModelCTest.java index 9034f9685..cc64855dc 100644 --- a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/BedOccupancyModelCTest.java +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/BedOccupancyModelCTest.java @@ -23,6 +23,7 @@ import java.util.concurrent.locks.ReentrantLock; import org.junit.Before; import org.junit.Test; import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.train.constant.AccommodationErrorCode; import com.ruoyi.train.domain.TrainAccommodation; import com.ruoyi.train.domain.TrainClassPlan; import com.ruoyi.train.domain.TrainDormitoryInfo; @@ -233,6 +234,7 @@ public class BedOccupancyModelCTest } catch (ServiceException e) { + assertEquals(Integer.valueOf(AccommodationErrorCode.BED_ALREADY_OCCUPIED), e.getCode()); assertTrue(e.getMessage().contains("BED_ALREADY_OCCUPIED")); } } @@ -263,6 +265,7 @@ public class BedOccupancyModelCTest } catch (ServiceException e) { + assertEquals(Integer.valueOf(AccommodationErrorCode.BED_ALREADY_OCCUPIED), e.getCode()); assertTrue(e.getMessage().contains("BED_ALREADY_OCCUPIED")); } } @@ -564,6 +567,7 @@ public class BedOccupancyModelCTest } catch (ServiceException e) { + assertEquals(Integer.valueOf(AccommodationErrorCode.BED_ALREADY_OCCUPIED), e.getCode()); assertTrue(e.getMessage().contains("BED_ALREADY_OCCUPIED")); } }