From aeb76ce35a75517d5de58acfa72fbd20630d076d Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Tue, 22 Sep 2026 17:03:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BD=8F=E5=AE=BF=EF=BC=9A?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=BD=8F=E5=AE=BF=E7=94=9F=E5=91=BD=E5=91=A8?= =?UTF-8?q?=E6=9C=9F=E7=8A=B6=E6=80=81=E4=B8=8E=E7=8A=B6=E6=80=81=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 住宿状态在数据库与业务上只有 0 预分配 / 1 已入住 / 2 已退宿三种,不存在 3 已调房。 调房是业务操作,真实语义为旧记录置退宿、新记录置在住,不应表现为独立状态值。 - 前端住宿页面移除筛选项与两处状态标签中的"已调房"(status=3),补齐历史表格缺失的预分配标签 - 后端通用 PUT /train/accommodation 封死 accommodation_status 写入路径, 状态变更只能经办理入住、办理退宿、调房等专属业务操作 - 新增住宿状态白名单守卫,非法整数与新增即退宿一律拒绝入库 Co-authored-by: Cursor --- .../impl/TrainAccommodationServiceImpl.java | 34 ++++++++++++++++ .../src/views/train/accommodation/index.vue | 5 +-- .../unit/accommodationStatusEnum.spec.mjs | 40 +++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 build_ui/tests/unit/accommodationStatusEnum.spec.mjs 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 dfcb6373e..6c87518a1 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 @@ -186,6 +186,12 @@ public class TrainAccommodationServiceImpl implements ITrainAccommodationService { accommodation.setAccommodationStatus(TrainAccommodation.STATUS_PREALLOCATED); } + // FIX-04: 住宿生命周期状态白名单守卫,杜绝非法整数与伪造状态落库 + assertStatusInLifecycleWhitelist(accommodation.getAccommodationStatus()); + if (TrainAccommodation.STATUS_CHECKED_OUT.equals(accommodation.getAccommodationStatus())) + { + throw new ServiceException("新增住宿安排不能直接置为已退宿状态,退宿请使用专属退宿业务操作"); + } int rows = trainAccommodationMapper.insertTrainAccommodation(accommodation); if (rows > 0 && accessAuthorizationOrchestrator != null) { @@ -223,6 +229,16 @@ public class TrainAccommodationServiceImpl implements ITrainAccommodationService throw new ServiceException("变更宿舍请使用专属调房换房流程 (transfer),禁止直接修改住宿记录的宿舍ID"); } + // FIX-04: 住宿生命周期状态只能通过办理入住/办理退宿/调房等专属业务操作改变, + // 通用 update 一律不得写 accommodation_status,避免绕过状态机与门禁撤权联动。 + if (accommodation.getAccommodationStatus() != null + && !accommodation.getAccommodationStatus().equals(exist.getAccommodationStatus())) + { + throw new ServiceException("变更住宿状态请使用办理入住、办理退宿或调房等专属业务操作,禁止直接修改住宿记录状态"); + } + // 与现状一致时同样不下发该字段,杜绝通用更新路径触碰生命周期状态列 + accommodation.setAccommodationStatus(null); + String targetDormId = accommodation.getDormId() != null ? accommodation.getDormId() : exist.getDormId(); String targetBedNo = accommodation.getBedNo() != null ? accommodation.getBedNo() : exist.getBedNo(); @@ -403,6 +419,24 @@ public class TrainAccommodationServiceImpl implements ITrainAccommodationService return trainAccommodationMapper.deleteTrainAccommodationByAccommodationId(accommodationId); } + /** + * FIX-04: 住宿生命周期状态白名单校验。 + *

+ * 数据库与业务只承认三个状态:0 预分配、1 已入住、2 已退宿。 + * 不存在"已调房"等第四种状态,调房是业务操作而非住宿状态。 + * + * @param status 待校验的住宿状态 + */ + private void assertStatusInLifecycleWhitelist(Integer status) + { + if (!TrainAccommodation.STATUS_PREALLOCATED.equals(status) + && !TrainAccommodation.STATUS_CHECKED_IN.equals(status) + && !TrainAccommodation.STATUS_CHECKED_OUT.equals(status)) + { + throw new ServiceException("住宿状态不合法,仅支持预分配、已入住、已退宿三种状态"); + } + } + /** * Model C: 校验床位在开班计划时间窗口内的占用情况 (Fail Closed) * diff --git a/build_ui/src/views/train/accommodation/index.vue b/build_ui/src/views/train/accommodation/index.vue index e0efb76b2..0cb313219 100644 --- a/build_ui/src/views/train/accommodation/index.vue +++ b/build_ui/src/views/train/accommodation/index.vue @@ -30,7 +30,6 @@ - @@ -66,11 +65,11 @@ + @@ -183,8 +182,8 @@ diff --git a/build_ui/tests/unit/accommodationStatusEnum.spec.mjs b/build_ui/tests/unit/accommodationStatusEnum.spec.mjs new file mode 100644 index 000000000..85d627306 --- /dev/null +++ b/build_ui/tests/unit/accommodationStatusEnum.spec.mjs @@ -0,0 +1,40 @@ +import assert from 'node:assert'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const pagePath = path.resolve(__dirname, '../../src/views/train/accommodation/index.vue'); +const source = fs.readFileSync(pagePath, 'utf8'); + +console.log('=== Running Accommodation Status Enum Tests (FIX-04) ==='); + +// Runtime Truth:train_accommodation.accommodation_status 仅有 0=预分配 / 1=在住 / 2=已退宿。 +// 不存在 3=已调房;调房的真实语义是旧记录置 2、新记录置 1。 +{ + assert.strictEqual(source.includes('已调房'), false, '页面不得再出现「已调房」状态文案'); + assert.strictEqual(/accommodationStatus\s*===\s*3/.test(source), false, '页面不得再存在 accommodationStatus === 3 分支'); + assert.strictEqual(/]*:value="3"/.test(source), false, '筛选项不得再提供 value=3 的住宿状态'); + console.log('✔ PASS: no phantom status 3 (已调房) remains in the accommodation page'); +} + +// 合法状态枚举必须完整保留 +{ + assert.strictEqual(//.test(source), true, '筛选项必须保留 0=预分配'); + assert.strictEqual(//.test(source), true, '筛选项必须保留 1=在住'); + assert.strictEqual(//.test(source), true, '筛选项必须保留 2=已退宿'); + + const statusOptions = source.match(//g) || []; + assert.strictEqual(statusOptions.length, 3, '住宿状态筛选项必须恰好为 3 个合法枚举值'); + console.log('✔ PASS: legal status enum 0/1/2 fully preserved'); +} + +// 调房仍然是业务操作,不得被误删 +{ + assert.strictEqual(source.includes('handleOpenTransfer'), true, '调房业务操作入口必须保留'); + assert.strictEqual(source.includes('transferAccommodation'), true, '调房后端调用必须保留'); + assert.strictEqual(source.includes('getAccommodationHistory'), true, '住宿履历查询必须保留'); + console.log('✔ PASS: room transfer stays a business operation, not a fake status'); +} + +console.log('=== All Accommodation Status Enum Unit Tests Passed ===');