diff --git a/mydreamjob-server/src/main/java/com/mydreamjob/auth/AdminBootstrapRunner.java b/mydreamjob-server/src/main/java/com/mydreamjob/auth/AdminBootstrapRunner.java index 58fb874..acf4de5 100644 --- a/mydreamjob-server/src/main/java/com/mydreamjob/auth/AdminBootstrapRunner.java +++ b/mydreamjob-server/src/main/java/com/mydreamjob/auth/AdminBootstrapRunner.java @@ -66,13 +66,9 @@ public class AdminBootstrapRunner implements ApplicationRunner { } phone = phone.trim(); - // 已有活动 ADMIN:无副作用跳过,绝不创建第二个管理员。 - if (userRepository.countByRoleAndActiveTrue(User.Role.ADMIN) > 0) { - log.info("admin bootstrap skipped: an active administrator already exists"); - return; - } - - // 手机号已被任何用户占用(含 inactive、STUDENT、COMPANY、角色不一致)? + // 配置手机号已被任何用户占用(含 inactive、STUDENT、COMPANY、角色不一致)? + // 必须先验证该账号,不能被“已有活动 ADMIN”短路:否则一个主角色为 ADMIN、 + // 但角色表缺失/含附加角色/仍有密码的账号会绕过严格幂等判据。 Optional existing = creator.findByPhone(phone); if (existing.isPresent()) { // 方案 A:仅当现存账号正是本次初始化配置要求的合法 ADMIN 时幂等成功; @@ -84,6 +80,12 @@ public class AdminBootstrapRunner implements ApplicationRunner { throw new IllegalStateException(ERROR_PHONE_IN_USE); } + // 配置手机号不存在且已有其他活动 ADMIN:无副作用跳过,绝不创建第二个管理员。 + if (userRepository.countByRoleAndActiveTrue(User.Role.ADMIN) > 0) { + log.info("admin bootstrap skipped: an active administrator already exists"); + return; + } + AdminBootstrapCreator.CreateResult result = creator.createAdmin(phone, properties.getNickname()); if (result == AdminBootstrapCreator.CreateResult.CONFLICT) { // 并发初始化:另一个实例刚创建了同一手机号。按方案 A 重新判定: diff --git a/mydreamjob-server/src/test/java/com/mydreamjob/auth/AdminBootstrapRunnerTest.java b/mydreamjob-server/src/test/java/com/mydreamjob/auth/AdminBootstrapRunnerTest.java index 98f6650..63bda52 100644 --- a/mydreamjob-server/src/test/java/com/mydreamjob/auth/AdminBootstrapRunnerTest.java +++ b/mydreamjob-server/src/test/java/com/mydreamjob/auth/AdminBootstrapRunnerTest.java @@ -140,7 +140,6 @@ class AdminBootstrapRunnerTest { @Test void phoneBelongingToStudentFails() { enableWith(PHONE); - when(userRepository.countByRoleAndActiveTrue(User.Role.ADMIN)).thenReturn(0L); when(creator.findByPhone(PHONE)).thenReturn(Optional.of( new AdminBootstrapCreator.BootstrapUser(9L, "STUDENT", true, true, true, List.of("STUDENT")))); @@ -152,7 +151,6 @@ class AdminBootstrapRunnerTest { @Test void phoneBelongingToCompanyFails() { enableWith(PHONE); - when(userRepository.countByRoleAndActiveTrue(User.Role.ADMIN)).thenReturn(0L); when(creator.findByPhone(PHONE)).thenReturn(Optional.of( new AdminBootstrapCreator.BootstrapUser(9L, "COMPANY", true, true, true, List.of("COMPANY")))); @@ -163,7 +161,6 @@ class AdminBootstrapRunnerTest { @Test void phoneBelongingToInactiveUserFailsAndIsNotRevived() { enableWith(PHONE); - when(userRepository.countByRoleAndActiveTrue(User.Role.ADMIN)).thenReturn(0L); when(creator.findByPhone(PHONE)).thenReturn(Optional.of( new AdminBootstrapCreator.BootstrapUser(9L, "ADMIN", false, true, true, List.of("ADMIN")))); @@ -171,6 +168,25 @@ class AdminBootstrapRunnerTest { verify(creator, never()).createAdmin(any(), any()); } + @Test + void activeAdminMissingAdminRoleCannotBypassStrictIdempotencyCheck() { + assertInvalidExistingAdminRejected( + new AdminBootstrapCreator.BootstrapUser(9L, "ADMIN", true, true, true, List.of())); + } + + @Test + void activeAdminWithAdditionalRoleCannotBypassStrictIdempotencyCheck() { + assertInvalidExistingAdminRejected( + new AdminBootstrapCreator.BootstrapUser( + 9L, "ADMIN", true, true, true, List.of("ADMIN", "STUDENT"))); + } + + @Test + void activeAdminWithPasswordCannotBypassStrictIdempotencyCheck() { + assertInvalidExistingAdminRejected( + new AdminBootstrapCreator.BootstrapUser(9L, "ADMIN", true, true, false, List.of("ADMIN"))); + } + // ── 正常创建 ─────────────────────────────────────────────── @Test @@ -222,7 +238,6 @@ class AdminBootstrapRunnerTest { @Test void failureMessagesNeverContainPhone() { enableWith(PHONE); - when(userRepository.countByRoleAndActiveTrue(User.Role.ADMIN)).thenReturn(0L); when(creator.findByPhone(PHONE)).thenReturn(Optional.of( new AdminBootstrapCreator.BootstrapUser(9L, "STUDENT", true, true, true, List.of("STUDENT")))); @@ -234,4 +249,14 @@ class AdminBootstrapRunnerTest { assertFalse(haystack != null && haystack.contains(needle), "输出不得包含手机号"); } + + private void assertInvalidExistingAdminRejected(AdminBootstrapCreator.BootstrapUser existing) { + enableWith(PHONE); + when(creator.findByPhone(PHONE)).thenReturn(Optional.of(existing)); + + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> runner.run(null)); + + assertEquals(AdminBootstrapRunner.ERROR_PHONE_IN_USE, ex.getMessage()); + verify(creator, never()).createAdmin(any(), any()); + } }