From 1d2ca54d269e6c3c6b8d7a7fa6cb68de4c828e94 Mon Sep 17 00:00:00 2001 From: wangzhibiao Date: Mon, 21 Sep 2026 14:57:05 +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=A2=9E=E5=8A=A0=E6=9C=89=E6=95=88=E5=BA=8A=E4=BD=8D=E5=8D=A0?= =?UTF-8?q?=E7=94=A8=E5=94=AF=E4=B8=80=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/TrainAccommodationMapper.java | 12 ++ .../impl/TrainAccommodationServiceImpl.java | 24 +++- .../mapper/train/TrainAccommodationMapper.xml | 11 ++ .../impl/AccommodationServiceTest.java | 25 ++++ .../kingbase/015_dms_package_a_hardening.sql | 119 ++++++++++++++++++ 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 RuoYi-Vue/sql/system/kingbase/015_dms_package_a_hardening.sql diff --git a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainAccommodationMapper.java b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainAccommodationMapper.java index 7506c37fa..d53566a0a 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainAccommodationMapper.java +++ b/RuoYi-Vue/ruoyi-train/src/main/java/com/ruoyi/train/mapper/TrainAccommodationMapper.java @@ -69,6 +69,18 @@ public interface TrainAccommodationMapper */ public int countActiveByDormId(@Param("dormId") String dormId); + /** + * 统计指定宿舍和床位当前处于有效状态 (0预分配, 1已入住) 的住宿记录数 + * + * @param dormId 宿舍ID + * @param bedNo 床位号 + * @param excludeAccommodationId 排除的自身住宿ID (更新时排除自身) + * @return 冲突占用数 + */ + public int countActiveByDormIdAndBedNo(@Param("dormId") String dormId, + @Param("bedNo") String bedNo, + @Param("excludeAccommodationId") Long excludeAccommodationId); + /** * 新增住宿安排 * 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 cad9e4dae..6234b2898 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 @@ -169,7 +169,17 @@ public class TrainAccommodationServiceImpl implements ITrainAccommodationService + ",当前有效人数: " + currentActive + "),无法继续分配"); } - // 5. 设置默认属性 + // 5. 验证床位唯一占用约束 (Active Bed Uniqueness Guard: MAJ-06) + if (accommodation.getBedNo() != null && !accommodation.getBedNo().trim().isEmpty()) + { + int bedOccupied = trainAccommodationMapper.countActiveByDormIdAndBedNo(dormId, accommodation.getBedNo().trim(), null); + if (bedOccupied > 0) + { + throw new ServiceException("BED_ALREADY_OCCUPIED: 该床位当前已被占用 (宿舍: " + dormId + ",床位: " + accommodation.getBedNo() + ")"); + } + } + + // 6. 设置默认属性 if (accommodation.getAssignedTime() == null) { accommodation.setAssignedTime(DateUtils.getNowDate()); @@ -215,6 +225,18 @@ public class TrainAccommodationServiceImpl implements ITrainAccommodationService throw new ServiceException("变更宿舍请使用专属调房换房流程 (transfer),禁止直接修改住宿记录的宿舍ID"); } + // 验证床位唯一占用约束 (更新时排除自身) + String targetDormId = accommodation.getDormId() != null ? accommodation.getDormId() : exist.getDormId(); + String targetBedNo = accommodation.getBedNo() != null ? accommodation.getBedNo().trim() : (exist.getBedNo() != null ? exist.getBedNo().trim() : null); + if (targetBedNo != null && !targetBedNo.isEmpty()) + { + int bedOccupied = trainAccommodationMapper.countActiveByDormIdAndBedNo(targetDormId, targetBedNo, exist.getAccommodationId()); + if (bedOccupied > 0) + { + throw new ServiceException("BED_ALREADY_OCCUPIED: 该床位当前已被占用 (宿舍: " + targetDormId + ",床位: " + targetBedNo + ")"); + } + } + accommodation.setUpdateTime(DateUtils.getNowDate()); return trainAccommodationMapper.updateTrainAccommodation(accommodation); } diff --git a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainAccommodationMapper.xml b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainAccommodationMapper.xml index 494b10e6d..c8ca7b33c 100644 --- a/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainAccommodationMapper.xml +++ b/RuoYi-Vue/ruoyi-train/src/main/resources/mapper/train/TrainAccommodationMapper.xml @@ -129,6 +129,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND accommodation_status IN (0, 1) + + INSERT INTO train_accommodation diff --git a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/AccommodationServiceTest.java b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/AccommodationServiceTest.java index d11f6c250..d0661e7e3 100644 --- a/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/AccommodationServiceTest.java +++ b/RuoYi-Vue/ruoyi-train/src/test/java/com/ruoyi/train/service/impl/AccommodationServiceTest.java @@ -609,6 +609,31 @@ public class AccommodationServiceTest return count; } + @Override + public int countActiveByDormIdAndBedNo(String dormId, String bedNo, Long excludeAccommodationId) + { + if (dormId == null || bedNo == null || bedNo.trim().isEmpty()) { + return 0; + } + int count = 0; + for (TrainAccommodation a : store.values()) + { + if (excludeAccommodationId != null && excludeAccommodationId.equals(a.getAccommodationId())) + { + continue; + } + if (dormId.equals(a.getDormId()) && bedNo.equals(a.getBedNo())) + { + if (TrainAccommodation.STATUS_PREALLOCATED.equals(a.getAccommodationStatus()) + || TrainAccommodation.STATUS_CHECKED_IN.equals(a.getAccommodationStatus())) + { + count++; + } + } + } + return count; + } + @Override public int insertTrainAccommodation(TrainAccommodation accommodation) { diff --git a/RuoYi-Vue/sql/system/kingbase/015_dms_package_a_hardening.sql b/RuoYi-Vue/sql/system/kingbase/015_dms_package_a_hardening.sql new file mode 100644 index 000000000..3695a67d6 --- /dev/null +++ b/RuoYi-Vue/sql/system/kingbase/015_dms_package_a_hardening.sql @@ -0,0 +1,119 @@ +-- ============================================================================ +-- Migration: 015_dms_package_a_hardening.sql +-- Description: Package A.1 DMS 一致性加固与最小权限拆分 +-- 1. 权限最小化拆分:新增 2194 (train:access:credential),从 train_admin 移除 2192 (train:access:manage) +-- 2. 床位唯一性约束:检测无重复记录时创建有效床位部分唯一索引 +-- 3. 幂等与断言校验 +-- ============================================================================ + +\set ON_ERROR_STOP on + +BEGIN; + +-- 1. 注册 2194 (train:access:credential) 门禁凭据下发与注销专用权限 +INSERT INTO public.sys_menu + (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, + menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +SELECT 2194, '门禁凭据操作', 2060, 4, '#', '', '', 1, 0, + 'F', '0', '0', 'train:access:credential', '#', 'admin', CURRENT_TIMESTAMP, '', NULL, '门禁凭据下发与注销权限' +WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2194); + +-- 2. 超级管理员角色 (role_id = 1) 授予 2194 +INSERT INTO public.sys_role_menu (role_id, menu_id) +SELECT 1, 2194 +WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 1 AND menu_id = 2194); + +-- 3. 培训管理员角色 (role_id = 3) 调整权限 (MIN-09): +-- 授予 2194 (train:access:credential) +INSERT INTO public.sys_role_menu (role_id, menu_id) +SELECT 3, 2194 +WHERE NOT EXISTS (SELECT 1 FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = 2194); + +-- 从 train_admin (role_id = 3) 回收 2192 (train:access:manage,硬件映射与维护权限) +DELETE FROM public.sys_role_menu +WHERE role_id = 3 AND menu_id = 2192; + +-- 从 train_admin (role_id = 3) 回收 2263 (train:accommodation:remove,物理删除住宿历史权限) +DELETE FROM public.sys_role_menu +WHERE role_id = 3 AND menu_id = 2263; + +-- 4. 有效床位唯一性约束 (Section 17) +-- 先只读检查是否存在已有的活跃床位重复数据 +DO $$ +DECLARE + v_dup_groups INTEGER; +BEGIN + SELECT COUNT(*) INTO v_dup_groups FROM ( + SELECT dorm_id, bed_no + FROM train_accommodation + WHERE accommodation_status IN (0, 1) + AND bed_no IS NOT NULL + GROUP BY dorm_id, bed_no + HAVING COUNT(*) > 1 + ) t; + + IF v_dup_groups = 0 THEN + IF NOT EXISTS ( + SELECT 1 FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = 'idx_train_acc_dorm_bed_active_uniq' + ) THEN + CREATE UNIQUE INDEX idx_train_acc_dorm_bed_active_uniq + ON train_accommodation (dorm_id, bed_no) + WHERE accommodation_status IN (0, 1) AND bed_no IS NOT NULL; + RAISE NOTICE 'Created partial unique index idx_train_acc_dorm_bed_active_uniq successfully.'; + END IF; + ELSE + RAISE NOTICE 'ACTIVE_BED_DUPLICATE_GROUP_COUNT = %, skipping unique index creation to prevent business data destruction.', v_dup_groups; + END IF; +END $$; + +-- 5. 状态机约束兼容性检查 (train_external_operation) +-- 确保 operation_status 允许 DISPATCHING 与 AMBIGUOUS +DO $$ +DECLARE + v_conname TEXT; +BEGIN + SELECT conname INTO v_conname + FROM pg_constraint + WHERE conrelid = 'train_external_operation'::regclass + AND contype = 'c' + AND pg_get_constraintdef(oid) LIKE '%operation_status%'; + + IF v_conname IS NOT NULL THEN + EXECUTE 'ALTER TABLE train_external_operation DROP CONSTRAINT ' || quote_ident(v_conname); + EXECUTE 'ALTER TABLE train_external_operation ADD CONSTRAINT chk_external_operation_status ' || + 'CHECK (operation_status IN (''CREATED'',''DISPATCHING'',''REQUESTED'',''ACCEPTED'',''PENDING'',''RETRY_WAIT'',''AMBIGUOUS'',''SUCCESS'',''FAILED'',''TIMEOUT''))'; + END IF; +END $$; + +-- 6. 后置断言验证 +DO $$ +BEGIN + -- 断言 1: 2194 已正确注册 + IF (SELECT count(*) FROM public.sys_menu WHERE menu_id = 2194 AND perms = 'train:access:credential' AND status = '0') <> 1 THEN + RAISE EXCEPTION 'Assertion failed: menu 2194 train:access:credential not found or invalid'; + END IF; + + -- 断言 2: train_admin (role_id = 3) 拥有 2194 (train:access:credential) + IF (SELECT count(*) FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = 2194) <> 1 THEN + RAISE EXCEPTION 'Assertion failed: role 3 not granted menu 2194'; + END IF; + + -- 断言 3: train_admin (role_id = 3) 不得拥有 2192 (train:access:manage) + IF (SELECT count(*) FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = 2192) <> 0 THEN + RAISE EXCEPTION 'Assertion failed: role 3 still has menu 2192 (train:access:manage)'; + END IF; + + -- 断言 4: train_admin (role_id = 3) 不得拥有 2263 (train:accommodation:remove) + IF (SELECT count(*) FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = 2263) <> 0 THEN + RAISE EXCEPTION 'Assertion failed: role 3 has forbidden menu 2263 (train:accommodation:remove)'; + END IF; + + -- 断言 5: train_admin 拥有 2264 (train:accommodation:add) + IF (SELECT count(*) FROM public.sys_role_menu WHERE role_id = 3 AND menu_id = 2264) <> 1 THEN + RAISE EXCEPTION 'Assertion failed: role 3 missing menu 2264 (train:accommodation:add)'; + END IF; +END $$; + +COMMIT;