修复住宿:统一住宿生命周期状态与状态变更入口
住宿状态在数据库与业务上只有 0 预分配 / 1 已入住 / 2 已退宿三种,不存在 3 已调房。 调房是业务操作,真实语义为旧记录置退宿、新记录置在住,不应表现为独立状态值。 - 前端住宿页面移除筛选项与两处状态标签中的"已调房"(status=3),补齐历史表格缺失的预分配标签 - 后端通用 PUT /train/accommodation 封死 accommodation_status 写入路径, 状态变更只能经办理入住、办理退宿、调房等专属业务操作 - 新增住宿状态白名单守卫,非法整数与新增即退宿一律拒绝入库 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
ecfd38be17
commit
aeb76ce35a
3 files changed
+76
-3
No files matched your search
+34
@@ -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: 住宿生命周期状态白名单校验。
|
||||
* <p>
|
||||
* 数据库与业务只承认三个状态: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)
|
||||
*
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
<el-option label="预分配" :value="0" />
|
||||
<el-option label="在住" :value="1" />
|
||||
<el-option label="已退宿" :value="2" />
|
||||
<el-option label="已调房" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
@@ -66,11 +65,11 @@
|
||||
<el-table-column label="房间号" align="center" prop="dormRoom" width="90" />
|
||||
<el-table-column label="床位号" align="center" prop="bedNo" width="80" />
|
||||
<el-table-column label="住宿状态" align="center" width="120">
|
||||
<!-- 住宿状态仅有 0=预分配 / 1=在住 / 2=已退宿 三种;调房由「旧记录置退宿 + 新记录置在住」表达,没有独立状态值 -->
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.accommodationStatus === 1" type="success" size="small">在住</el-tag>
|
||||
<el-tag v-else-if="scope.row.accommodationStatus === 0" type="info" size="small">预分配</el-tag>
|
||||
<el-tag v-else-if="scope.row.accommodationStatus === 2" type="danger" size="small">已退宿</el-tag>
|
||||
<el-tag v-else-if="scope.row.accommodationStatus === 3" type="warning" size="small">已调房</el-tag>
|
||||
<el-tag v-else size="small">{{ scope.row.accommodationStatus }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -183,8 +182,8 @@
|
||||
<el-table-column label="住宿状态" align="center" width="110">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.accommodationStatus === 1" type="success" size="mini">在住</el-tag>
|
||||
<el-tag v-else-if="scope.row.accommodationStatus === 0" type="info" size="mini">预分配</el-tag>
|
||||
<el-tag v-else-if="scope.row.accommodationStatus === 2" type="danger" size="mini">已退宿</el-tag>
|
||||
<el-tag v-else-if="scope.row.accommodationStatus === 3" type="warning" size="mini">已调房</el-tag>
|
||||
<el-tag v-else size="mini">{{ scope.row.accommodationStatus }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -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(/<el-option[^>]*:value="3"/.test(source), false, '筛选项不得再提供 value=3 的住宿状态');
|
||||
console.log('✔ PASS: no phantom status 3 (已调房) remains in the accommodation page');
|
||||
}
|
||||
|
||||
// 合法状态枚举必须完整保留
|
||||
{
|
||||
assert.strictEqual(/<el-option label="预分配" :value="0" \/>/.test(source), true, '筛选项必须保留 0=预分配');
|
||||
assert.strictEqual(/<el-option label="在住" :value="1" \/>/.test(source), true, '筛选项必须保留 1=在住');
|
||||
assert.strictEqual(/<el-option label="已退宿" :value="2" \/>/.test(source), true, '筛选项必须保留 2=已退宿');
|
||||
|
||||
const statusOptions = source.match(/<el-option label="[^"]+" :value="\d" \/>/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 ===');
|
||||
Reference in new issue
Block a user