修复稳定化:接入考勤人工调整闭环并增加住宿与门禁历史删除防护

This commit is contained in:
wangzhibiao committed 2026-09-20 14:09:45 +08:00
1 parent 5bcb6273ef
commit b1433dcaa1
7 files changed
+491 -9

No files matched your search

@@ -0,0 +1,233 @@
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.Field;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PutMapping;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.train.domain.TrainAccommodation;
import com.ruoyi.train.domain.TrainDormAuth;
import com.ruoyi.train.domain.dto.AttendanceAdjustDTO;
import com.ruoyi.train.mapper.TrainAccommodationMapper;
import com.ruoyi.train.mapper.TrainDormAuthMapper;
import com.ruoyi.train.service.impl.TrainAccommodationServiceImpl;
import com.ruoyi.train.service.impl.TrainDormAuthServiceImpl;
/**
* 试用前最终内部缺陷修复 (PRE-CUSTOMER FINAL INTERNAL FIXES) 契约与防护测试:
* 1. 考勤人工修正 Controller 契约与权限一致性 (train:attendance:edit)
* 2. 住宿历史记录物理删除拦截 (ACCOMMODATION_HISTORY_PHYSICAL_DELETE_BLOCKED)
* 3. 门禁注销授权与操作流水物理删除拦截 (DOOR_AUTH_HISTORY_PHYSICAL_DELETE_BLOCKED)
*/
public class PreCustomerStabilizationFinalFixesContractTest
{
private TrainAccommodationServiceImpl accommodationService;
private TrainDormAuthServiceImpl dormAuthService;
private Map<Long, TrainAccommodation> accommodationStore;
private Map<Long, TrainDormAuth> authStore;
@Before
public void setUp() throws Exception
{
accommodationService = new TrainAccommodationServiceImpl();
dormAuthService = new TrainDormAuthServiceImpl();
accommodationStore = new HashMap<>();
authStore = new HashMap<>();
TrainAccommodationMapper mockAccMapper = (TrainAccommodationMapper) java.lang.reflect.Proxy.newProxyInstance(
TrainAccommodationMapper.class.getClassLoader(),
new Class<?>[]{TrainAccommodationMapper.class},
(proxy, method, args) -> {
String name = method.getName();
if ("selectTrainAccommodationByAccommodationId".equals(name))
{
return accommodationStore.get(args[0]);
}
if ("deleteTrainAccommodationByAccommodationId".equals(name))
{
return accommodationStore.remove(args[0]) != null ? 1 : 0;
}
if (method.getReturnType().equals(int.class)) return 0;
return null;
}
);
TrainDormAuthMapper mockAuthMapper = (TrainDormAuthMapper) java.lang.reflect.Proxy.newProxyInstance(
TrainDormAuthMapper.class.getClassLoader(),
new Class<?>[]{TrainDormAuthMapper.class},
(proxy, method, args) -> {
String name = method.getName();
if ("selectTrainDormAuthByAuthId".equals(name))
{
return authStore.get(args[0]);
}
if ("deleteTrainDormAuthByAuthId".equals(name))
{
return authStore.remove(args[0]) != null ? 1 : 0;
}
if (method.getReturnType().equals(int.class)) return 0;
return null;
}
);
Field accField = TrainAccommodationServiceImpl.class.getDeclaredField("trainAccommodationMapper");
accField.setAccessible(true);
accField.set(accommodationService, mockAccMapper);
Field authField = TrainDormAuthServiceImpl.class.getDeclaredField("trainDormAuthMapper");
authField.setAccessible(true);
authField.set(dormAuthService, mockAuthMapper);
}
/**
* 1. 验证 AttendanceController 的人工调整契约与权限注解严格为 train:attendance:edit
*/
@Test
public void testAttendanceAdjustControllerContract() throws Exception
{
Method adjustMethod = AttendanceController.class.getMethod("adjust", Long.class, AttendanceAdjustDTO.class);
assertNotNull("adjust 方法必须存在", adjustMethod);
PutMapping putMapping = adjustMethod.getAnnotation(PutMapping.class);
assertNotNull("必须声明 @PutMapping", putMapping);
assertEquals("/{checkinId}", putMapping.value()[0]);
PreAuthorize preAuth = adjustMethod.getAnnotation(PreAuthorize.class);
assertNotNull("必须声明 @PreAuthorize 权限校验", preAuth);
assertEquals("@ss.hasPermi('train:attendance:edit')", preAuth.value());
}
/**
* 2. 住宿历史删除防护:
* A. 预分配错误/尚未真实入住:允许物理清理
* B. 活跃在住或已退宿真实历史:强行拦截并保护历史事实
*/
@Test
public void testAccommodationHistoricalDeleteGuard()
{
// Case A: 预分配错误 (STATUS_PREALLOCATED = 0)
TrainAccommodation draft = new TrainAccommodation();
draft.setAccommodationId(9001L);
draft.setAccommodationStatus(TrainAccommodation.STATUS_PREALLOCATED);
accommodationStore.put(draft.getAccommodationId(), draft);
int draftDeleted = accommodationService.deleteTrainAccommodationByAccommodationId(9001L);
assertEquals("未实际入住的预分配记录允许纠错删除", 1, draftDeleted);
// Case B1: 活跃在住 (STATUS_CHECKED_IN = 1)
TrainAccommodation active = new TrainAccommodation();
active.setAccommodationId(9002L);
active.setAccommodationStatus(TrainAccommodation.STATUS_CHECKED_IN);
accommodationStore.put(active.getAccommodationId(), active);
try
{
accommodationService.deleteTrainAccommodationByAccommodationId(9002L);
fail("在住记录禁止物理删除");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("当前学员正处于已入住状态"));
}
// Case B2: 已退宿历史记录 (STATUS_CHECKED_OUT = 2) - 核心历史履历防护
TrainAccommodation historical = new TrainAccommodation();
historical.setAccommodationId(9003L);
historical.setAccommodationStatus(TrainAccommodation.STATUS_CHECKED_OUT);
accommodationStore.put(historical.getAccommodationId(), historical);
try
{
accommodationService.deleteTrainAccommodationByAccommodationId(9003L);
fail("已退宿真实历史记录禁止物理删除,必须保留历史履历事实");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("禁止物理删除历史履历")
|| e.getMessage().contains("真实历史入住与退宿事实"));
}
assertNotNull("历史退宿记录在库中依然完好保存", accommodationStore.get(9003L));
}
/**
* 3. 门禁注销授权与操作流水物理删除拦截防护:
* A. 初始草稿尚未产生操作:允许删除
* B. 生效中/授权中:必须先注销
* C. 已注销/已关联外部流水:严禁物理删除,保全审计溯源链
*/
@Test
public void testDoorAuthHistoryPhysicalDeleteGuard()
{
// Case A: 初始草稿待下发 (STATUS_PENDING / AUTH_STATUS_PENDING_GRANT)
TrainDormAuth draft = new TrainDormAuth();
draft.setAuthId(8001L);
draft.setFaceSyncStatus(TrainDormAuth.STATUS_PENDING);
draft.setAuthStatus(TrainDormAuth.AUTH_STATUS_PENDING_GRANT);
authStore.put(draft.getAuthId(), draft);
int draftDeleted = dormAuthService.deleteTrainDormAuthByAuthId(8001L);
assertEquals("无外部流水草稿允许清理", 1, draftDeleted);
// Case B: 生效中授权 (ACTIVE)
TrainDormAuth active = new TrainDormAuth();
active.setAuthId(8002L);
active.setFaceSyncStatus(TrainDormAuth.STATUS_ACTIVE);
active.setAuthStatus(TrainDormAuth.AUTH_STATUS_ACTIVE);
authStore.put(active.getAuthId(), active);
try
{
dormAuthService.deleteTrainDormAuthByAuthId(8002L);
fail("生效中门禁授权禁止直接删除");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("当前门禁授权处于生效或授权中状态"));
}
// Case C: 已注销授权 (REVOKED) - 核心安全审计链防护
TrainDormAuth revoked = new TrainDormAuth();
revoked.setAuthId(8003L);
revoked.setFaceSyncStatus(TrainDormAuth.STATUS_REVOKED);
revoked.setAuthStatus(TrainDormAuth.AUTH_STATUS_REVOKED);
authStore.put(revoked.getAuthId(), revoked);
try
{
dormAuthService.deleteTrainDormAuthByAuthId(8003L);
fail("已注销门禁授权严禁物理删除,以保障审计链完整");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("禁止物理删除历史凭证")
|| e.getMessage().contains("已进入授权生命周期"));
}
assertNotNull("已注销门禁授权依然完好存在", authStore.get(8003L));
// Case D: 带有外部操作流水关联 (latestOperationId != null)
TrainDormAuth opLinked = new TrainDormAuth();
opLinked.setAuthId(8004L);
opLinked.setLatestOperationId(6666L);
authStore.put(opLinked.getAuthId(), opLinked);
try
{
dormAuthService.deleteTrainDormAuthByAuthId(8004L);
fail("关联外部流水的门禁授权严禁物理删除");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("产生外部审计流水"));
}
}
}