修复V4目录搜索与幂等审计
This commit is contained in:
@@ -13,6 +13,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@@ -87,14 +88,16 @@ public class MybatisReservationV4CatalogRepository implements ReservationV4Catal
|
||||
String keyword,
|
||||
int pageNum,
|
||||
int pageSize) {
|
||||
String normalizedKeyword = trimToNull(keyword);
|
||||
String normalizedCodeKeyword = normalizeCode(normalizedKeyword);
|
||||
LambdaQueryWrapper<ReservationCatalogAccountEntity> query = Wrappers.<ReservationCatalogAccountEntity>lambdaQuery()
|
||||
.eq(ReservationCatalogAccountEntity::getHotelId, hotelId)
|
||||
.eq(ReservationCatalogAccountEntity::getStatus, ReservationV4CatalogStatus.ACTIVE.name())
|
||||
.isNull(ReservationCatalogAccountEntity::getLogicDeletedAt)
|
||||
.and(trimToNull(keyword) != null, wrapper -> wrapper
|
||||
.like(ReservationCatalogAccountEntity::getAccountCode, trimToNull(keyword))
|
||||
.and(normalizedKeyword != null, wrapper -> wrapper
|
||||
.like(ReservationCatalogAccountEntity::getAccountCode, normalizedCodeKeyword)
|
||||
.or()
|
||||
.like(ReservationCatalogAccountEntity::getAccountName, trimToNull(keyword)))
|
||||
.like(ReservationCatalogAccountEntity::getAccountName, normalizedKeyword))
|
||||
.orderByAsc(ReservationCatalogAccountEntity::getAccountCode)
|
||||
.orderByAsc(ReservationCatalogAccountEntity::getId);
|
||||
Page<ReservationCatalogAccountEntity> page = accountMapper.selectPage(Page.of(pageNum, pageSize), query);
|
||||
@@ -114,15 +117,17 @@ public class MybatisReservationV4CatalogRepository implements ReservationV4Catal
|
||||
String keyword,
|
||||
int pageNum,
|
||||
int pageSize) {
|
||||
String normalizedKeyword = trimToNull(keyword);
|
||||
String normalizedCodeKeyword = normalizeCode(normalizedKeyword);
|
||||
LambdaQueryWrapper<ReservationCatalogCodeEntity> query = Wrappers.<ReservationCatalogCodeEntity>lambdaQuery()
|
||||
.eq(ReservationCatalogCodeEntity::getHotelId, hotelId)
|
||||
.eq(ReservationCatalogCodeEntity::getCatalogType, catalogType)
|
||||
.eq(ReservationCatalogCodeEntity::getStatus, ReservationV4CatalogStatus.ACTIVE.name())
|
||||
.isNull(ReservationCatalogCodeEntity::getLogicDeletedAt)
|
||||
.and(trimToNull(keyword) != null, wrapper -> wrapper
|
||||
.like(ReservationCatalogCodeEntity::getCode, trimToNull(keyword))
|
||||
.and(normalizedKeyword != null, wrapper -> wrapper
|
||||
.like(ReservationCatalogCodeEntity::getCode, normalizedCodeKeyword)
|
||||
.or()
|
||||
.like(ReservationCatalogCodeEntity::getDisplayName, trimToNull(keyword)))
|
||||
.like(ReservationCatalogCodeEntity::getDisplayName, normalizedKeyword))
|
||||
.orderByAsc(ReservationCatalogCodeEntity::getSortOrder)
|
||||
.orderByAsc(ReservationCatalogCodeEntity::getCode)
|
||||
.orderByAsc(ReservationCatalogCodeEntity::getId);
|
||||
@@ -144,13 +149,14 @@ public class MybatisReservationV4CatalogRepository implements ReservationV4Catal
|
||||
int pageNum,
|
||||
int pageSize) {
|
||||
String normalizedKeyword = trimToNull(keyword);
|
||||
String normalizedCodeKeyword = normalizeCode(normalizedKeyword);
|
||||
String normalizedStatus = trimToNull(status);
|
||||
LambdaQueryWrapper<ReservationCatalogAccountEntity> query = Wrappers.<ReservationCatalogAccountEntity>lambdaQuery()
|
||||
.eq(ReservationCatalogAccountEntity::getHotelId, hotelId)
|
||||
.eq(normalizedStatus != null, ReservationCatalogAccountEntity::getStatus, normalizedStatus)
|
||||
.isNull(ReservationCatalogAccountEntity::getLogicDeletedAt)
|
||||
.and(normalizedKeyword != null, wrapper -> wrapper
|
||||
.like(ReservationCatalogAccountEntity::getAccountCode, normalizedKeyword)
|
||||
.like(ReservationCatalogAccountEntity::getAccountCode, normalizedCodeKeyword)
|
||||
.or()
|
||||
.like(ReservationCatalogAccountEntity::getAccountName, normalizedKeyword))
|
||||
.orderByAsc(ReservationCatalogAccountEntity::getAccountCode)
|
||||
@@ -174,6 +180,7 @@ public class MybatisReservationV4CatalogRepository implements ReservationV4Catal
|
||||
int pageNum,
|
||||
int pageSize) {
|
||||
String normalizedKeyword = trimToNull(keyword);
|
||||
String normalizedCodeKeyword = normalizeCode(normalizedKeyword);
|
||||
String normalizedStatus = trimToNull(status);
|
||||
LambdaQueryWrapper<ReservationCatalogCodeEntity> query = Wrappers.<ReservationCatalogCodeEntity>lambdaQuery()
|
||||
.eq(ReservationCatalogCodeEntity::getHotelId, hotelId)
|
||||
@@ -181,7 +188,7 @@ public class MybatisReservationV4CatalogRepository implements ReservationV4Catal
|
||||
.eq(normalizedStatus != null, ReservationCatalogCodeEntity::getStatus, normalizedStatus)
|
||||
.isNull(ReservationCatalogCodeEntity::getLogicDeletedAt)
|
||||
.and(normalizedKeyword != null, wrapper -> wrapper
|
||||
.like(ReservationCatalogCodeEntity::getCode, normalizedKeyword)
|
||||
.like(ReservationCatalogCodeEntity::getCode, normalizedCodeKeyword)
|
||||
.or()
|
||||
.like(ReservationCatalogCodeEntity::getDisplayName, normalizedKeyword))
|
||||
.orderByAsc(ReservationCatalogCodeEntity::getSortOrder)
|
||||
@@ -433,4 +440,9 @@ public class MybatisReservationV4CatalogRepository implements ReservationV4Catal
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String normalizeCode(String value) {
|
||||
String normalized = trimToNull(value);
|
||||
return normalized == null ? null : normalized.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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;
|
||||
@@ -22,6 +23,7 @@ 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.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
@@ -60,6 +62,9 @@ class ReservationV4CatalogAdminControllerTest {
|
||||
@Autowired
|
||||
private AuthPasswordService passwordService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private String adminToken;
|
||||
private String noPermissionToken;
|
||||
|
||||
@@ -188,6 +193,30 @@ class ReservationV4CatalogAdminControllerTest {
|
||||
.andReturn();
|
||||
String rateCodeId = objectMapper.readTree(rateCreated.getResponse().getContentAsString()).path("id").asText();
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/room-types")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_id":"HOTEL-TEST",
|
||||
"code":"CP1_ROOM",
|
||||
"display_name":"CP1 Room Duplicate"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error_code").value("RESERVATION_CATALOG_CONFLICT"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/rate-codes")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_id":"HOTEL-TEST",
|
||||
"code":"CP1_RATE",
|
||||
"display_name":"CP1 Rate Duplicate"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error_code").value("RESERVATION_CATALOG_CONFLICT"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), put("/api/admin/reservation/catalogs/rate-codes/{catalogId}/status", rateCodeId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
@@ -257,6 +286,100 @@ class ReservationV4CatalogAdminControllerTest {
|
||||
.andExpect(jsonPath("$.items[0].code").value("CP1_DISABLE_ROOM"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSearchCatalogAdminCodeIgnoringKeywordCase() throws Exception {
|
||||
performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/accounts")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_id":"HOTEL-TEST",
|
||||
"account_code":"CP1_CASE_ACCOUNT",
|
||||
"account_name":"CP1 Case Account",
|
||||
"market_code":"LEISURE",
|
||||
"source_code":"TRAVEL_AGENT"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/room-types")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_id":"HOTEL-TEST",
|
||||
"code":"CP1_CASE_ROOM",
|
||||
"display_name":"CP1 Case Room"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/rate-codes")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_id":"HOTEL-TEST",
|
||||
"code":"CP1_CASE_RATE",
|
||||
"display_name":"CP1 Case Rate"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/admin/reservation/catalogs/accounts")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "cp1_case_account"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].account_code").value("CP1_CASE_ACCOUNT"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/admin/reservation/catalogs/room-types")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "cp1_case_room"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].code").value("CP1_CASE_ROOM"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/admin/reservation/catalogs/rate-codes")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "cp1_case_rate"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].code").value("CP1_CASE_RATE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotWriteAuditWhenStatusUpdateIsNoop() throws Exception {
|
||||
MvcResult created = performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/room-types")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_id":"HOTEL-TEST",
|
||||
"code":"CP1_NOOP_AUDIT_ROOM",
|
||||
"display_name":"CP1 Noop Audit Room"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
String roomTypeId = objectMapper.readTree(created.getResponse().getContentAsString()).path("id").asText();
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), put("/api/admin/reservation/catalogs/room-types/{catalogId}/status", roomTypeId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"status":"DISABLED"}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("DISABLED"));
|
||||
|
||||
Long afterFirstUpdate = countCatalogStatusAudits(roomTypeId, "UPDATE_ROOM_TYPE_CATALOG_STATUS");
|
||||
assertThat(afterFirstUpdate).isEqualTo(1L);
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), put("/api/admin/reservation/catalogs/room-types/{catalogId}/status", roomTypeId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"status":"DISABLED"}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("DISABLED"));
|
||||
|
||||
Long afterNoopUpdate = countCatalogStatusAudits(roomTypeId, "UPDATE_ROOM_TYPE_CATALOG_STATUS");
|
||||
assertThat(afterNoopUpdate).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectCatalogAdminWithoutPermissionOrHotelAccess() throws Exception {
|
||||
mockMvc.perform(get("/api/admin/reservation/catalogs/accounts")
|
||||
@@ -287,4 +410,17 @@ class ReservationV4CatalogAdminControllerTest {
|
||||
}
|
||||
return noPermissionToken;
|
||||
}
|
||||
|
||||
private Long countCatalogStatusAudits(String targetId, String action) {
|
||||
return jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(1)
|
||||
FROM platform_admin_audit_log
|
||||
WHERE target_type = 'RESERVATION_V4_CATALOG_CODE'
|
||||
AND target_id = ?
|
||||
AND action = ?
|
||||
""",
|
||||
Long.class,
|
||||
targetId,
|
||||
action);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,27 @@ class ReservationV4CatalogLookupControllerTest {
|
||||
.andExpect(jsonPath("$.items[0].pricing_available").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSearchLookupCodeIgnoringKeywordCase() throws Exception {
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/accounts")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "qbd_travel"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].code").value("QBD_TRAVEL"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/room-types")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "rm2"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].code").value("RM2"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/rate-codes")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "group"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].code").value("GROUP"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepCatalogMetadataWhenKeywordMatchesNothing() throws Exception {
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/accounts")
|
||||
|
||||
Reference in New Issue
Block a user