测试业务:补齐状态并发与批量新增真实行为验证

This commit is contained in:
wangzhibiao committed 2026-09-21 21:43:53 +08:00
1 parent 612f24d5f1
commit fd8e0f7477
8 files changed
+550 -8

No files matched your search

@@ -125,7 +125,7 @@ public class BusinessTransactionIndependenceTest {
new Class<?>[]{TrainEnrollmentMapper.class}, new Class<?>[]{TrainEnrollmentMapper.class},
(proxy, method, args) -> { (proxy, method, args) -> {
String name = method.getName(); String name = method.getName();
if ("selectTrainEnrollmentById".equals(name)) return enrollment; if ("selectTrainEnrollmentById".equals(name) || "selectTrainEnrollmentByIdForUpdate".equals(name)) return enrollment;
if ("confirmEnrollmentReport".equals(name)) { if ("confirmEnrollmentReport".equals(name)) {
enrollment.setEnrollmentStatus(1); enrollment.setEnrollmentStatus(1);
return 1; return 1;
@@ -71,6 +71,22 @@ public class AttendanceActionContractTest
checkinsByEnrollLesson.put(c.getEnrollmentId() + "_" + c.getLessonId(), c); checkinsByEnrollLesson.put(c.getEnrollmentId() + "_" + c.getLessonId(), c);
return 1; return 1;
} }
if ("signOutIfUnsigned".equals(name))
{
Long cid = (Long) args[0];
Date soTime = (Date) args[1];
String op = (String) args[2];
Date ut = (Date) args[3];
TrainStudentCheckin c = checkinsById.get(cid);
if (c != null && c.getSignInTime() != null && c.getSignOutTime() == null)
{
c.setSignOutTime(soTime);
c.setUpdateBy(op);
c.setUpdateTime(ut);
return 1;
}
return 0;
}
if ("updateTrainStudentCheckin".equals(name)) if ("updateTrainStudentCheckin".equals(name))
{ {
TrainStudentCheckin c = (TrainStudentCheckin) args[0]; TrainStudentCheckin c = (TrainStudentCheckin) args[0];
@@ -88,7 +104,7 @@ public class AttendanceActionContractTest
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{ {
String name = method.getName(); String name = method.getName();
if ("selectTrainEnrollmentById".equals(name)) if ("selectTrainEnrollmentById".equals(name) || "selectTrainEnrollmentByIdForUpdate".equals(name))
{ {
return enrollments.get((Long) args[0]); return enrollments.get((Long) args[0]);
} }
@@ -748,6 +748,27 @@ public class EnrollmentAppServiceImplTest
return store.get(enrollmentId); return store.get(enrollmentId);
} }
@Override
public TrainEnrollment selectTrainEnrollmentByIdForUpdate(Long enrollmentId)
{
return store.get(enrollmentId);
}
@Override
public List<TrainEnrollment> selectEnrollmentsForUpdateByPlanId(String planId)
{
List<TrainEnrollment> list = new ArrayList<>();
for (TrainEnrollment e : store.values())
{
if (e.getPlanId().equals(planId))
{
list.add(e);
}
}
list.sort(java.util.Comparator.comparing(TrainEnrollment::getEnrollmentId));
return list;
}
@Override @Override
public TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId) public TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId)
{ {
@@ -1096,6 +1117,18 @@ public class EnrollmentAppServiceImplTest
return enrollmentService.selectTrainEnrollmentById(enrollmentId); return enrollmentService.selectTrainEnrollmentById(enrollmentId);
} }
@Override
public TrainEnrollment selectTrainEnrollmentByIdForUpdate(Long enrollmentId)
{
return enrollmentService.selectTrainEnrollmentByIdForUpdate(enrollmentId);
}
@Override
public List<TrainEnrollment> selectEnrollmentsForUpdateByPlanId(String planId)
{
return enrollmentService.selectEnrollmentsForUpdateByPlanId(planId);
}
@Override @Override
public TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId) public TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId)
{ {
@@ -56,6 +56,7 @@ public class LegacyAnalyticsV2ServiceTest
public int updateTrainStudentCheckin(TrainStudentCheckin checkin) { return 0; } public int updateTrainStudentCheckin(TrainStudentCheckin checkin) { return 0; }
public List<TrainStudentCheckin> selectCheckinsByStudentId(String sId, String pId, String m) { return Collections.emptyList(); } public List<TrainStudentCheckin> selectCheckinsByStudentId(String sId, String pId, String m) { return Collections.emptyList(); }
public List<TrainStudentCheckin> selectCheckinsByLessonId(Long lId) { return Collections.emptyList(); } public List<TrainStudentCheckin> selectCheckinsByLessonId(Long lId) { return Collections.emptyList(); }
public int signOutIfUnsigned(Long cId, java.util.Date soTime, String op, java.util.Date ut) { return 0; }
}); });
assertEquals(0, service.selectTrainStudentCheckinList(new TrainStudentCheckin()).size()); assertEquals(0, service.selectTrainStudentCheckinList(new TrainStudentCheckin()).size());
TrainStudentCheckin query = new TrainStudentCheckin(); TrainStudentCheckin query = new TrainStudentCheckin();
@@ -50,6 +50,10 @@ public class PackageBCoreBusinessBehaviorTest
// DMS Reconcile 监控计数 // DMS Reconcile 监控计数
private List<Long> dmsReconcileInvocations; private List<Long> dmsReconcileInvocations;
// 行锁调用追踪
private List<Long> selectForUpdateInvocations;
private List<String> planEnrollmentsForUpdateInvocations;
@Before @Before
public void setUp() public void setUp()
{ {
@@ -61,6 +65,8 @@ public class PackageBCoreBusinessBehaviorTest
planCourseTable = new HashMap<>(); planCourseTable = new HashMap<>();
studentTable = new HashMap<>(); studentTable = new HashMap<>();
dmsReconcileInvocations = new ArrayList<>(); dmsReconcileInvocations = new ArrayList<>();
selectForUpdateInvocations = new ArrayList<>();
planEnrollmentsForUpdateInvocations = new ArrayList<>();
// 初始化基础班级、课程与学员 // 初始化基础班级、课程与学员
TrainClassPlan defaultPlan = new TrainClassPlan(); TrainClassPlan defaultPlan = new TrainClassPlan();
@@ -100,6 +106,27 @@ public class PackageBCoreBusinessBehaviorTest
@Override @Override
public TrainEnrollment selectTrainEnrollmentById(Long id) { return enrollmentTable.get(id); } public TrainEnrollment selectTrainEnrollmentById(Long id) { return enrollmentTable.get(id); }
@Override @Override
public TrainEnrollment selectTrainEnrollmentByIdForUpdate(Long id)
{
selectForUpdateInvocations.add(id);
return enrollmentTable.get(id);
}
@Override
public List<TrainEnrollment> selectEnrollmentsForUpdateByPlanId(String planId)
{
planEnrollmentsForUpdateInvocations.add(planId);
List<TrainEnrollment> list = new ArrayList<>();
for (TrainEnrollment e : enrollmentTable.values())
{
if (e.getPlanId().equals(planId))
{
list.add(e);
}
}
list.sort(Comparator.comparing(TrainEnrollment::getEnrollmentId));
return list;
}
@Override
public int updateEnrollmentStatus(Long id, Integer status, String remark, String op, Date time) public int updateEnrollmentStatus(Long id, Integer status, String remark, String op, Date time)
{ {
TrainEnrollment e = enrollmentTable.get(id); TrainEnrollment e = enrollmentTable.get(id);
@@ -522,7 +549,7 @@ public class PackageBCoreBusinessBehaviorTest
assertTrue("batchInsertTrainEnrollment 必须固定为 0", assertTrue("batchInsertTrainEnrollment 必须固定为 0",
batchSnippet.contains("#{item.sourceType}, 0,")); batchSnippet.contains("#{item.sourceType}, 0,"));
// 2. 行为验证:输入 DTO 携带 status=4 或 1 时,批量入库后状态一律固定为 0 // 2. 真实 Service 行为验证:输入 DTO 携带 status=4 或 1 时,经过 Service 处理后传递给 Mapper 的状态一律固定为 0
TrainEnrollment item1 = new TrainEnrollment(); TrainEnrollment item1 = new TrainEnrollment();
item1.setPlanId("PLAN-001"); item1.setPlanId("PLAN-001");
item1.setStudentId("STU-001"); item1.setStudentId("STU-001");
@@ -534,9 +561,36 @@ public class PackageBCoreBusinessBehaviorTest
item2.setEnrollmentStatus(1); // 恶意输入1 item2.setEnrollmentStatus(1); // 恶意输入1
List<TrainEnrollment> list = Arrays.asList(item1, item2); List<TrainEnrollment> list = Arrays.asList(item1, item2);
for (TrainEnrollment item : list) item.setEnrollmentStatus(0); final List<TrainEnrollment> captured = new ArrayList<>();
assertEquals(Integer.valueOf(0), item1.getEnrollmentStatus());
assertEquals(Integer.valueOf(0), item2.getEnrollmentStatus()); TrainEnrollmentMapper capturingMapper = (TrainEnrollmentMapper) Proxy.newProxyInstance(
TrainEnrollmentMapper.class.getClassLoader(),
new Class<?>[]{TrainEnrollmentMapper.class},
(proxy, method, args) -> {
if ("batchInsertTrainEnrollment".equals(method.getName()))
{
List<TrainEnrollment> passed = (List<TrainEnrollment>) args[0];
for (TrainEnrollment e : passed)
{
TrainEnrollment copy = new TrainEnrollment();
copy.setStudentId(e.getStudentId());
copy.setEnrollmentStatus(e.getEnrollmentStatus());
captured.add(copy);
}
return passed.size();
}
return null;
}
);
TrainEnrollmentServiceImpl realService = new TrainEnrollmentServiceImpl();
setField(realService, "trainEnrollmentMapper", capturingMapper);
realService.batchInsertTrainEnrollment(list);
assertEquals("Mapper 必须收到 2 条记录", 2, captured.size());
assertEquals("即使输入为4,Service传递给Mapper的第1条状态必须强制为0", Integer.valueOf(0), captured.get(0).getEnrollmentStatus());
assertEquals("即使输入为1,Service传递给Mapper的第2条状态必须强制为0", Integer.valueOf(0), captured.get(1).getEnrollmentStatus());
} }
// ========================================================================= // =========================================================================
@@ -935,6 +989,209 @@ public class PackageBCoreBusinessBehaviorTest
normalRowsBefore, normalRowsAfter); normalRowsBefore, normalRowsAfter);
} }
// =========================================================================
// 30. 并发硬化与事务契约测试 (B28 ~ B34)
// =========================================================================
/** B28 & B29: 并发双 signOut 原子条件更新 CAS 守卫:最多1次成功,不能覆盖首次时间 (MOCK_CONCURRENT_BEHAVIOR_TEST) */
@Test
public void testB28_B29_ConcurrentSignOutAtomicGuard() throws Exception
{
Long enrollmentId = 8801L;
Long lessonId = 1001L;
createEnrollment(enrollmentId, "PLAN-001", "STU-8801", 1);
Date signInTime = new Date(System.currentTimeMillis() - 7200000); // 2小时前
createCheckin(enrollmentId, lessonId, signInTime);
int threadCount = 2;
java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(threadCount);
java.util.concurrent.CyclicBarrier barrier = new java.util.concurrent.CyclicBarrier(threadCount);
java.util.concurrent.atomic.AtomicInteger successCount = new java.util.concurrent.atomic.AtomicInteger(0);
java.util.concurrent.atomic.AtomicInteger failureCount = new java.util.concurrent.atomic.AtomicInteger(0);
java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(threadCount);
Date time1 = new Date(System.currentTimeMillis() - 3600000); // 1小时前
Date time2 = new Date(); // 当前时间
executor.submit(() -> {
try {
barrier.await();
AttendanceSignOutDTO dto = new AttendanceSignOutDTO();
dto.setEnrollmentId(enrollmentId);
dto.setLessonId(lessonId);
dto.setSignOutTime(time1);
attendanceAppService.signOut(dto, "thread-1");
successCount.incrementAndGet();
} catch (Exception e) {
failureCount.incrementAndGet();
} finally {
latch.countDown();
}
});
executor.submit(() -> {
try {
barrier.await();
AttendanceSignOutDTO dto = new AttendanceSignOutDTO();
dto.setEnrollmentId(enrollmentId);
dto.setLessonId(lessonId);
dto.setSignOutTime(time2);
attendanceAppService.signOut(dto, "thread-2");
successCount.incrementAndGet();
} catch (Exception e) {
failureCount.incrementAndGet();
} finally {
latch.countDown();
}
});
latch.await(5, java.util.concurrent.TimeUnit.SECONDS);
executor.shutdown();
assertEquals("并发双签退只能有1次成功", 1, successCount.get());
assertEquals("并发双签退必须有1次失败被拒绝", 1, failureCount.get());
TrainStudentCheckin checkin = checkinTable.values().stream()
.filter(c -> c.getEnrollmentId().equals(enrollmentId) && c.getLessonId().equals(lessonId))
.findFirst().orElse(null);
assertNotNull(checkin);
assertNotNull("必须已记录签退时间", checkin.getSignOutTime());
assertTrue("签退时间不能被覆盖", checkin.getSignOutTime().equals(time1) || checkin.getSignOutTime().equals(time2));
}
/** B30: updateEnrollmentStatus 使用 enrollment FOR UPDATE 消除 TOCTOU */
@Test
public void testB30_UpdateEnrollmentStatusUsesForUpdateLock()
{
selectForUpdateInvocations.clear();
Long enrollmentId = 301L;
createEnrollment(enrollmentId, "PLAN-001", "STU-301", 1);
createReport(enrollmentId, 301L);
EnrollmentStatusUpdateDTO dto = new EnrollmentStatusUpdateDTO();
dto.setEnrollmentStatus(2); // 请假
dto.setRemark("事假");
enrollmentAppService.updateEnrollmentStatus(enrollmentId, dto, "admin");
assertTrue("updateEnrollmentStatus 必须通过 selectTrainEnrollmentByIdForUpdate 获取行级锁",
selectForUpdateInvocations.contains(enrollmentId));
}
/** B31: confirmReport 使用同一 enrollment FOR UPDATE 消除 TOCTOU */
@Test
public void testB31_ConfirmReportUsesForUpdateLock()
{
selectForUpdateInvocations.clear();
Long enrollmentId = 302L;
createEnrollment(enrollmentId, "PLAN-001", "STU-302", 0);
ReportConfirmDTO dto = new ReportConfirmDTO();
dto.setReportWay(4);
enrollmentAppService.confirmReport(enrollmentId, dto, "admin");
assertTrue("confirmReport 必须通过 selectTrainEnrollmentByIdForUpdate 获取行级锁",
selectForUpdateInvocations.contains(enrollmentId));
}
/** B32: plan completion 必须先按稳定顺序锁定该 plan 下全部 enrollment 行,再执行 precheck */
@Test
public void testB32_PlanCompletionLocksAllEnrollmentsBeforePrecheck()
{
planEnrollmentsForUpdateInvocations.clear();
String planId = "PLAN-B32";
TrainClassPlan plan = new TrainClassPlan();
plan.setPlanId(planId);
plan.setPlanStatus(1);
planTable.put(planId, plan);
createEnrollment(321L, planId, "STU-321", 1);
createReport(321L, 321L);
planAppService.completePlan(planId, "lead_admin");
assertTrue("结训预检必须调用 selectEnrollmentsForUpdateByPlanId 锁定全班学员记录",
planEnrollmentsForUpdateInvocations.contains(planId));
}
/** B33: batchInsert 真实 Service 调用强制 status=0 拦截非法入参 */
@Test
public void testB33_BatchInsertRealServiceForcesZero()
{
TrainEnrollment e1 = new TrainEnrollment();
e1.setPlanId("PLAN-001");
e1.setStudentId("STU-331");
e1.setEnrollmentStatus(4);
final List<TrainEnrollment> captured = new ArrayList<>();
TrainEnrollmentMapper capturingMapper = (TrainEnrollmentMapper) Proxy.newProxyInstance(
TrainEnrollmentMapper.class.getClassLoader(),
new Class<?>[]{TrainEnrollmentMapper.class},
(proxy, method, args) -> {
if ("batchInsertTrainEnrollment".equals(method.getName()))
{
List<TrainEnrollment> passed = (List<TrainEnrollment>) args[0];
for (TrainEnrollment item : passed) {
TrainEnrollment copy = new TrainEnrollment();
copy.setEnrollmentStatus(item.getEnrollmentStatus());
captured.add(copy);
}
return passed.size();
}
return null;
}
);
TrainEnrollmentServiceImpl realService = new TrainEnrollmentServiceImpl();
setField(realService, "trainEnrollmentMapper", capturingMapper);
realService.batchInsertTrainEnrollment(Collections.singletonList(e1));
assertEquals(1, captured.size());
assertEquals("Service 必须把 enrollmentStatus 强制覆写为 0", Integer.valueOf(0), captured.get(0).getEnrollmentStatus());
}
/** B34: adapter 异常必须完整向外传播,禁止 PlanAppServiceImpl 吃掉异常继续完成 */
@Test
public void testB34_AdapterExceptionPropagatesOutward()
{
String planId = "PLAN-B34";
TrainClassPlan plan = new TrainClassPlan();
plan.setPlanId(planId);
plan.setPlanStatus(1);
planTable.put(planId, plan);
createEnrollment(341L, planId, "STU-341", 0); // 待报到,预检必然抛出异常
try
{
planAppService.completePlan(planId, "operator");
fail("Adapter 预检异常必须向外抛出,禁止结训成功");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("WAITING_REPORT_ENROLLMENTS_EXIST"));
// 验证计划自身状态绝未被修改为完成 (2)
assertEquals("计划状态绝不能被更新为完成", Integer.valueOf(1), planTable.get(planId).getPlanStatus());
}
}
/** 事务注解契约验证:验证 completePlan 为事务入口,completeEnrollments 为 MANDATORY 事务 */
@Test
public void testPlanCompletionTransactionAnnotationContract() throws Exception
{
java.lang.reflect.Method completePlanMethod = PlanAppServiceImpl.class.getMethod("completePlan", String.class, String.class);
org.springframework.transaction.annotation.Transactional planTx =
completePlanMethod.getAnnotation(org.springframework.transaction.annotation.Transactional.class);
assertNotNull("PlanAppServiceImpl.completePlan 必须标记 @Transactional", planTx);
assertEquals(Exception.class, planTx.rollbackFor()[0]);
java.lang.reflect.Method completeEnrollmentsMethod = EnrollmentPlanCompletionAdapter.class.getMethod("completeEnrollments", String.class, String.class, Date.class);
org.springframework.transaction.annotation.Transactional enrollTx =
completeEnrollmentsMethod.getAnnotation(org.springframework.transaction.annotation.Transactional.class);
assertNotNull("EnrollmentPlanCompletionAdapter.completeEnrollments 必须标记 @Transactional", enrollTx);
assertEquals("结训学员归档事务传播机制必须为 MANDATORY",
org.springframework.transaction.annotation.Propagation.MANDATORY, enrollTx.propagation());
}
// ========================================================================= // =========================================================================
// Helper Methods // Helper Methods
// ========================================================================= // =========================================================================
@@ -962,6 +1219,33 @@ public class PackageBCoreBusinessBehaviorTest
return r; return r;
} }
private TrainStudentCheckin createCheckin(Long enrollmentId, Long lessonId, Date signInTime)
{
TrainStudentCheckin c = new TrainStudentCheckin();
long id = checkinTable.size() + 100L;
c.setCheckinId(id);
c.setEnrollmentId(enrollmentId);
c.setLessonId(lessonId);
c.setSignInTime(signInTime);
c.setAttendanceStatus(1);
checkinTable.put(id, c);
return c;
}
private void setField(Object target, String fieldName, Object value)
{
try
{
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
catch (Exception e)
{
throw new RuntimeException("Failed to set field " + fieldName + " on " + target, e);
}
}
private TrainEnrollmentMapper createMockEnrollmentMapper() private TrainEnrollmentMapper createMockEnrollmentMapper()
{ {
return (TrainEnrollmentMapper) Proxy.newProxyInstance( return (TrainEnrollmentMapper) Proxy.newProxyInstance(
@@ -970,6 +1254,23 @@ public class PackageBCoreBusinessBehaviorTest
(proxy, method, args) -> { (proxy, method, args) -> {
String name = method.getName(); String name = method.getName();
if ("selectTrainEnrollmentById".equals(name)) return enrollmentTable.get(args[0]); if ("selectTrainEnrollmentById".equals(name)) return enrollmentTable.get(args[0]);
if ("selectTrainEnrollmentByIdForUpdate".equals(name))
{
selectForUpdateInvocations.add((Long) args[0]);
return enrollmentTable.get(args[0]);
}
if ("selectEnrollmentsForUpdateByPlanId".equals(name))
{
String pid = (String) args[0];
planEnrollmentsForUpdateInvocations.add(pid);
List<TrainEnrollment> list = new ArrayList<>();
for (TrainEnrollment e : enrollmentTable.values())
{
if (e.getPlanId().equals(pid)) list.add(e);
}
list.sort(Comparator.comparing(TrainEnrollment::getEnrollmentId));
return list;
}
if ("confirmEnrollmentReport".equals(name)) if ("confirmEnrollmentReport".equals(name))
{ {
Long eid = (Long) args[0]; Long eid = (Long) args[0];
@@ -1042,10 +1343,18 @@ public class PackageBCoreBusinessBehaviorTest
TrainClassPlanMapper.class.getClassLoader(), TrainClassPlanMapper.class.getClassLoader(),
new Class<?>[]{TrainClassPlanMapper.class}, new Class<?>[]{TrainClassPlanMapper.class},
(proxy, method, args) -> { (proxy, method, args) -> {
if ("selectTrainClassPlanByPlanId".equals(method.getName()) || "selectPlanForUpdate".equals(method.getName())) if ("selectTrainClassPlanByPlanId".equals(method.getName()) || "selectPlanForUpdate".equals(method.getName())
|| "selectTrainClassPlanByPlanIdForUpdate".equals(method.getName()))
{ {
return planTable.get(args[0]); return planTable.get(args[0]);
} }
if ("updateTrainClassPlanStatus".equals(method.getName()))
{
TrainClassPlan p = (TrainClassPlan) args[0];
TrainClassPlan exist = planTable.get(p.getPlanId());
if (exist != null) exist.setPlanStatus(p.getPlanStatus());
return 1;
}
return null; return null;
} }
); );
@@ -1080,6 +1389,25 @@ public class PackageBCoreBusinessBehaviorTest
checkinTable.put(id, c); checkinTable.put(id, c);
return 1; return 1;
} }
if ("signOutIfUnsigned".equals(name))
{
Long cid = (Long) args[0];
Date soTime = (Date) args[1];
String op = (String) args[2];
Date ut = (Date) args[3];
synchronized (checkinTable)
{
TrainStudentCheckin c = checkinTable.get(cid);
if (c != null && c.getSignInTime() != null && c.getSignOutTime() == null)
{
c.setSignOutTime(soTime);
c.setUpdateBy(op);
c.setUpdateTime(ut);
return 1;
}
return 0;
}
}
if ("updateTrainStudentCheckin".equals(name)) if ("updateTrainStudentCheckin".equals(name))
{ {
TrainStudentCheckin c = (TrainStudentCheckin) args[0]; TrainStudentCheckin c = (TrainStudentCheckin) args[0];
@@ -308,6 +308,27 @@ public class PlanEnrollmentCompletionIntegrationTest
return enrollmentStore.get(enrollmentId); return enrollmentStore.get(enrollmentId);
} }
@Override
public TrainEnrollment selectTrainEnrollmentByIdForUpdate(Long enrollmentId)
{
return enrollmentStore.get(enrollmentId);
}
@Override
public List<TrainEnrollment> selectEnrollmentsForUpdateByPlanId(String planId)
{
List<TrainEnrollment> list = new ArrayList<>();
for (TrainEnrollment e : enrollmentStore.values())
{
if (e.getPlanId().equals(planId))
{
list.add(e);
}
}
list.sort(java.util.Comparator.comparing(TrainEnrollment::getEnrollmentId));
return list;
}
@Override @Override
public TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId) public TrainEnrollment selectTrainEnrollmentByPlanIdAndStudentId(String planId, String studentId)
{ {
@@ -53,7 +53,7 @@ public class ReportActionContractTest
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{ {
String name = method.getName(); String name = method.getName();
if ("selectTrainEnrollmentById".equals(name)) if ("selectTrainEnrollmentById".equals(name) || "selectTrainEnrollmentByIdForUpdate".equals(name))
{ {
return enrollments.get((Long) args[0]); return enrollments.get((Long) args[0]);
} }
+143
View File
@@ -0,0 +1,143 @@
package scripts.qa;
import java.io.File;
import java.nio.file.Files;
import java.sql.*;
/**
* 真实 Kingbase 数据库 ROLLBACK-ONLY 事务回滚验证脚本。
* 严禁 COMMIT 任何测试数据。验证真实数据库事务原子性与回滚机制。
*/
public class ValidateRealDbRollback {
public static void main(String[] args) throws Exception {
Class.forName("com.kingbase8.Driver");
File envFile = new File(System.getProperty("user.home"), ".railwaysystem-qa.env");
String pass = "";
if (envFile.exists()) {
for (String line : Files.readAllLines(envFile.toPath())) {
line = line.trim();
if (line.startsWith("QA_DB_PASSWORD=")) {
pass = line.substring("QA_DB_PASSWORD=".length()).trim();
if (pass.startsWith("\"") && pass.endsWith("\"")) {
pass = pass.substring(1, pass.length() - 1);
}
}
}
}
if (pass.isEmpty()) {
throw new IllegalStateException("QA_DB_PASSWORD is required in ~/.railwaysystem-qa.env");
}
// 尝试直连或通过 SSH 端口转发 (14321) 连接
String dbUrl = "jdbc:kingbase8://127.0.0.1:14321/railwaysystem_integrated_qa_20260916?useUnicode=true&characterEncoding=utf8";
try (Connection conn = DriverManager.getConnection(dbUrl, "kingbase", pass)) {
System.out.println("CONNECTED_TO_DB=TRUE");
// 1. 验证当前数据库身份
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT current_database(), current_user")) {
if (rs.next()) {
System.out.println("CURRENT_DATABASE=" + rs.getString(1));
System.out.println("CURRENT_USER=" + rs.getString(2));
if (!"railwaysystem_integrated_qa_20260916".equals(rs.getString(1))) {
throw new IllegalStateException("数据库身份不符,安全终止!");
}
}
}
// 2. 选择一条安全的已有记录读取原始状态
String targetPlanId = null;
int originalPlanStatus = -1;
long targetEnrollmentId = -1;
int originalEnrollmentStatus = -1;
String selectSampleSql = "SELECT p.plan_id, p.plan_status, e.enrollment_id, e.enrollment_status " +
"FROM train_class_plan p " +
"JOIN train_enrollment e ON e.plan_id = p.plan_id " +
"LIMIT 1";
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(selectSampleSql)) {
if (rs.next()) {
targetPlanId = rs.getString(1);
originalPlanStatus = rs.getInt(2);
targetEnrollmentId = rs.getLong(3);
originalEnrollmentStatus = rs.getInt(4);
}
}
if (targetPlanId == null) {
System.out.println("NO_SAMPLE_DATA_FOUND");
return;
}
System.out.println(String.format("SAMPLE_BEFORE: plan_id=%s, plan_status=%d, enrollment_id=%d, enrollment_status=%d",
targetPlanId, originalPlanStatus, targetEnrollmentId, originalEnrollmentStatus));
// 3. 开启事务 (禁用 AutoCommit)
conn.setAutoCommit(false);
try {
// 在未提交事务中模拟结训更新
int mockPlanTargetStatus = (originalPlanStatus == 2) ? 1 : 2;
int mockEnrollmentTargetStatus = (originalEnrollmentStatus == 4) ? 1 : 4;
try (PreparedStatement pst = conn.prepareStatement(
"UPDATE train_class_plan SET plan_status = ?, update_time = now() WHERE plan_id = ?")) {
pst.setInt(1, mockPlanTargetStatus);
pst.setString(2, targetPlanId);
pst.executeUpdate();
}
try (PreparedStatement pst = conn.prepareStatement(
"UPDATE train_enrollment SET enrollment_status = ?, complete_time = now() WHERE enrollment_id = ?")) {
pst.setInt(1, mockEnrollmentTargetStatus);
pst.setLong(2, targetEnrollmentId);
pst.executeUpdate();
}
// 事务内查看临时修改值
int inTxPlanStatus = -1;
int inTxEnrollStatus = -1;
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT p.plan_status, e.enrollment_status FROM train_class_plan p JOIN train_enrollment e ON e.plan_id = p.plan_id WHERE p.plan_id = '" + targetPlanId + "' AND e.enrollment_id = " + targetEnrollmentId)) {
if (rs.next()) {
inTxPlanStatus = rs.getInt(1);
inTxEnrollStatus = rs.getInt(2);
}
}
System.out.println(String.format("IN_TRANSACTION_MODIFIED: plan_status=%d, enrollment_status=%d", inTxPlanStatus, inTxEnrollStatus));
// 4. 显式执行主动 ROLLBACK (严禁 COMMIT)
conn.rollback();
System.out.println("TRANSACTION_ROLLED_BACK=TRUE");
} catch (Exception ex) {
conn.rollback();
throw ex;
} finally {
conn.setAutoCommit(true);
}
// 5. 验证回滚后数据完全恢复原样
int afterRollbackPlanStatus = -1;
int afterRollbackEnrollStatus = -1;
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT p.plan_status, e.enrollment_status FROM train_class_plan p JOIN train_enrollment e ON e.plan_id = p.plan_id WHERE p.plan_id = '" + targetPlanId + "' AND e.enrollment_id = " + targetEnrollmentId)) {
if (rs.next()) {
afterRollbackPlanStatus = rs.getInt(1);
afterRollbackEnrollStatus = rs.getInt(2);
}
}
System.out.println(String.format("AFTER_ROLLBACK_VERIFIED: plan_status=%d, enrollment_status=%d", afterRollbackPlanStatus, afterRollbackEnrollStatus));
boolean intact = (originalPlanStatus == afterRollbackPlanStatus) && (originalEnrollmentStatus == afterRollbackEnrollStatus);
if (intact) {
System.out.println("REAL_DB_TRANSACTION_ROLLBACK_VALIDATION=PASS");
} else {
System.err.println("REAL_DB_TRANSACTION_ROLLBACK_VALIDATION=FAIL (Data altered!)");
System.exit(1);
}
}
}
}