实现系统管理后台 V1

This commit is contained in:
andy
2026-07-10 18:32:34 +08:00
parent e76cb80f28
commit aa5783ba61
92 changed files with 9383 additions and 21 deletions

View File

@@ -81,6 +81,7 @@ class AuthControllerTest {
"SYSTEM_ROLE_MANAGE",
"SYSTEM_MENU_MANAGE",
"HOTEL_MANAGE",
"SYSTEM_ADMIN_CONSOLE_ACCESS",
"SYSTEM_DEBUG_EML_RUN")))
.andExpect(jsonPath("$.menus[0].menu_code").value("RESERVATION_ORDERS"))
.andExpect(jsonPath("$.menus[0].route_path").value("/reservation/orders"))
@@ -91,6 +92,9 @@ class AuthControllerTest {
.andExpect(jsonPath("$.menus[2].menu_code").value("DEBUG_EML_SUPERAGENT"))
.andExpect(jsonPath("$.menus[2].route_path").value("/debug/eml-superagent"))
.andExpect(jsonPath("$.menus[2].icon_key").value("pi pi-upload"))
.andExpect(jsonPath("$.menus[3].menu_code").value("SYSTEM_SETTINGS"))
.andExpect(jsonPath("$.menus[3].route_path").value("/system"))
.andExpect(jsonPath("$.menus[3].icon_key").value("pi pi-cog"))
.andReturn();
String token = tokenFrom(loginResult);
@@ -100,7 +104,7 @@ class AuthControllerTest {
.andExpect(status().isOk())
.andExpect(jsonPath("$.user.username").value("m003-admin"))
.andExpect(jsonPath("$.default_hotel_id").value("HOTEL-TEST"))
.andExpect(jsonPath("$.menus[2].menu_code").value("DEBUG_EML_SUPERAGENT"));
.andExpect(jsonPath("$.menus[3].menu_code").value("SYSTEM_SETTINGS"));
}
@Test

View File

@@ -0,0 +1,418 @@
package cn.nianxx.thhotel.platform.system.control;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import cn.nianxx.thhotel.ThHotelApplication;
import cn.nianxx.thhotel.platform.access.common.enums.PlatformRoleCode;
import cn.nianxx.thhotel.platform.access.domain.PlatformPermissionEntity;
import cn.nianxx.thhotel.platform.access.domain.PlatformRoleEntity;
import cn.nianxx.thhotel.platform.access.repository.PlatformAccessRepository;
import cn.nianxx.thhotel.platform.hotel.repository.PlatformHotelRepository;
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
import cn.nianxx.thhotel.platform.identity.repository.PlatformIdentityRepository;
import cn.nianxx.thhotel.platform.identity.service.impl.AuthPasswordService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
@SpringBootTest(
classes = ThHotelApplication.class,
properties = {
"spring.datasource.url=jdbc:h2:mem:m006_admin_readonly;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
"auth.bootstrap.admin.username=m006-admin",
"auth.bootstrap.admin.password=Admin@123456",
"auth.bootstrap.admin.display-name=系统管理员",
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
"auth.bootstrap.default-hotel-name=测试酒店",
"auth.bootstrap.default-hotel-time-zone=Asia/Bangkok"
})
@AutoConfigureMockMvc
@ActiveProfiles("test")
class AdminReadonlyControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private PlatformIdentityRepository identityRepository;
@Autowired
private PlatformAccessRepository accessRepository;
@Autowired
private PlatformHotelRepository hotelRepository;
@Autowired
private AuthPasswordService passwordService;
@BeforeEach
void ensureViewerUser() {
PlatformUserEntity user = identityRepository.findUserByUsername("m006-viewer")
.orElseGet(() -> {
LocalDateTime now = LocalDateTime.now();
PlatformUserEntity created = new PlatformUserEntity();
created.setUsername("m006-viewer");
created.setPasswordHash(passwordService.hash("Viewer@123456"));
created.setDisplayName("只读用户");
created.setUserStatus(PlatformUserStatus.ACTIVE.name());
created.setSuperAdmin(false);
created.setPasswordChangedAt(now);
created.setCreatedAt(now);
created.setUpdatedAt(now);
identityRepository.insertUser(created);
return created;
});
PlatformRoleEntity viewerRole = accessRepository.findRoleByCode(PlatformRoleCode.RESERVATION_VIEWER.name())
.orElseThrow();
accessRepository.ensureUserRole(user.getId(), viewerRole.getId());
hotelRepository.ensureUserHotel(user.getId(), "HOTEL-TEST", true);
}
@Test
void shouldRejectAdminEndpointWhenTokenMissing() throws Exception {
mockMvc.perform(get("/api/admin/users")
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.error_code").value("ADMIN_AUTH_REQUIRED"));
}
@Test
void shouldRejectAdminEndpointWhenTokenInvalid() throws Exception {
mockMvc.perform(get("/api/admin/users")
.header("Authorization", "Bearer invalid-token")
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.error_code").value("AUTH_SESSION_INVALID"));
}
@Test
void shouldAllowSystemAdminToReadAdminLists() throws Exception {
String token = tokenFrom(login("m006-admin", "Admin@123456"));
mockMvc.perform(get("/api/admin/users")
.header("Authorization", "Bearer " + token)
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items.length()", greaterThanOrEqualTo(1)))
.andExpect(jsonPath("$.page.page_num").value(1))
.andExpect(jsonPath("$.page.page_size").value(20));
mockMvc.perform(get("/api/admin/roles")
.header("Authorization", "Bearer " + token)
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].role_code").value(notNullValue()));
mockMvc.perform(get("/api/admin/permissions")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].permission_code").value(notNullValue()));
mockMvc.perform(get("/api/admin/menus")
.header("Authorization", "Bearer " + token)
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[?(@.menu_code=='SYSTEM_SETTINGS')]").exists());
mockMvc.perform(get("/api/admin/hotels")
.header("Authorization", "Bearer " + token)
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].hotel_id").value("HOTEL-TEST"));
}
@Test
void shouldRejectAdminEndpointWhenPermissionMissing() throws Exception {
String token = tokenFrom(login("m006-viewer", "Viewer@123456"));
mockMvc.perform(get("/api/admin/users")
.header("Authorization", "Bearer " + token)
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.error_code").value("ADMIN_PERMISSION_DENIED"));
}
@Test
void shouldCreateUserAssignHotelsResetPasswordAndRevokeSessionWhenDisabled() throws Exception {
String token = tokenFrom(login("m006-admin", "Admin@123456"));
PlatformRoleEntity viewerRole = accessRepository.findRoleByCode(PlatformRoleCode.RESERVATION_VIEWER.name())
.orElseThrow();
String username = "m006-user-" + System.nanoTime();
MvcResult createResult = mockMvc.perform(post("/api/admin/users")
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"username": "%s",
"initial_password": "User@123456",
"display_name": "测试用户",
"user_status": "ACTIVE",
"role_ids": ["%s"],
"hotel_ids": ["HOTEL-TEST"],
"default_hotel_id": "HOTEL-TEST"
}
""".formatted(username, viewerRole.getId())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value(username))
.andExpect(jsonPath("$.roles[0].role_code").value(PlatformRoleCode.RESERVATION_VIEWER.name()))
.andExpect(jsonPath("$.hotels[0].hotel_id").value("HOTEL-TEST"))
.andReturn();
String userId = objectMapper.readTree(createResult.getResponse().getContentAsString()).path("id").asText();
String userToken = tokenFrom(login(username, "User@123456"));
mockMvc.perform(post("/api/admin/users/{userId}/password-reset", userId)
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.user_id").value(userId))
.andExpect(jsonPath("$.temporary_password").value(notNullValue()));
mockMvc.perform(put("/api/admin/users/{userId}/roles", userId)
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"role_ids": []
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.roles.length()").value(0));
mockMvc.perform(put("/api/admin/users/{userId}/hotels", userId)
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_ids": ["HOTEL-TEST"],
"default_hotel_id": "HOTEL-TEST"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.hotels[0].default_hotel").value(true));
String disabledHotelId = "M006-DISABLED-" + System.nanoTime();
mockMvc.perform(post("/api/admin/hotels")
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_id": "%s",
"hotel_name": "M006 禁用酒店",
"time_zone": "Asia/Bangkok",
"sort_order": 199
}
""".formatted(disabledHotelId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.hotel_status").value("DISABLED"));
mockMvc.perform(put("/api/admin/users/{userId}/hotels", userId)
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_ids": ["HOTEL-TEST", "%s"],
"default_hotel_id": "HOTEL-TEST"
}
""".formatted(disabledHotelId)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("ADMIN_INVALID_REQUEST"));
mockMvc.perform(put("/api/admin/users/{userId}", userId)
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"display_name": "测试用户已禁用",
"user_status": "DISABLED"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.user_status").value("DISABLED"));
mockMvc.perform(get("/api/admin/audits")
.header("Authorization", "Bearer " + token)
.param("target_type", "PLATFORM_USER")
.param("target_id", userId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].target_type").value("PLATFORM_USER"));
mockMvc.perform(get("/api/auth/me")
.header("Authorization", "Bearer " + userToken))
.andExpect(status().isUnauthorized());
}
@Test
void shouldManageCustomRoleAndRejectBuiltinRoleMutation() throws Exception {
String token = tokenFrom(login("m006-admin", "Admin@123456"));
String roleCode = "M006_CUSTOM_" + System.nanoTime();
PlatformPermissionEntity permission = accessRepository.findPermissionByCode("SYSTEM_USER_MANAGE").orElseThrow();
MvcResult createResult = mockMvc.perform(post("/api/admin/roles")
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"role_code": "%s",
"role_name": "M006 自定义角色",
"role_status": "ACTIVE"
}
""".formatted(roleCode)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.role_code").value(roleCode))
.andExpect(jsonPath("$.system_builtin").value(false))
.andReturn();
String roleId = objectMapper.readTree(createResult.getResponse().getContentAsString()).path("id").asText();
mockMvc.perform(put("/api/admin/roles/{roleId}/permissions", roleId)
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"permission_ids": ["%s"]
}
""".formatted(permission.getId())))
.andExpect(status().isOk())
.andExpect(jsonPath("$.permissions[0].permission_code").value("SYSTEM_USER_MANAGE"));
PlatformRoleEntity builtinRole = accessRepository.findRoleByCode(PlatformRoleCode.SYSTEM_ADMIN.name())
.orElseThrow();
mockMvc.perform(put("/api/admin/roles/{roleId}", builtinRole.getId().toString())
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"role_name": "不允许修改",
"role_status": "ACTIVE"
}
"""))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.error_code").value("ADMIN_CONFLICT"));
}
@Test
void shouldCreateUnknownMenuAndKeepKnownRouteFlagFalse() throws Exception {
String token = tokenFrom(login("m006-admin", "Admin@123456"));
String menuCode = "M006_MENU_" + System.nanoTime();
MvcResult createResult = mockMvc.perform(post("/api/admin/menus")
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"menu_code": "%s",
"menu_name": "未来菜单",
"menu_type": "PAGE",
"route_path": "/future/not-ready",
"permission_code": "SYSTEM_USER_MANAGE",
"visible": true,
"menu_status": "ACTIVE"
}
""".formatted(menuCode)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.menu_code").value(menuCode))
.andExpect(jsonPath("$.known_route").value(false))
.andReturn();
String menuId = objectMapper.readTree(createResult.getResponse().getContentAsString()).path("id").asText();
mockMvc.perform(put("/api/admin/menus/{menuId}", menuId)
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"menu_name": "未来菜单更新",
"menu_type": "PAGE",
"route_path": "/system/users",
"permission_code": "SYSTEM_USER_MANAGE",
"visible": true,
"menu_status": "ACTIVE"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.menu_code").value(menuCode))
.andExpect(jsonPath("$.known_route").value(true));
}
@Test
void shouldCreateDisabledHotelAndRejectBreakingSingleActiveHotelRule() throws Exception {
String token = tokenFrom(login("m006-admin", "Admin@123456"));
String hotelId = "M006-HOTEL-" + System.nanoTime();
mockMvc.perform(post("/api/admin/hotels")
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_id": "%s",
"hotel_name": "M006 测试酒店",
"time_zone": "Asia/Bangkok",
"sort_order": 99
}
""".formatted(hotelId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.hotel_id").value(hotelId))
.andExpect(jsonPath("$.hotel_status").value("DISABLED"));
mockMvc.perform(put("/api/admin/hotels/{hotelId}/status", hotelId)
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_status": "ACTIVE"
}
"""))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.error_code").value("ADMIN_CONFLICT"));
mockMvc.perform(put("/api/admin/hotels/{hotelId}/status", "HOTEL-TEST")
.header("Authorization", "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_status": "DISABLED"
}
"""))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.error_code").value("ADMIN_CONFLICT"));
}
private MvcResult login(String username, String password) throws Exception {
return mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"username": "%s",
"password": "%s"
}
""".formatted(username, password)))
.andExpect(status().isOk())
.andReturn();
}
private String tokenFrom(MvcResult result) throws Exception {
JsonNode json = objectMapper.readTree(result.getResponse().getContentAsString());
return json.path("access_token").asText();
}
}