修复Rooming List生成并导入0718需求包
This commit is contained in:
@@ -10,6 +10,7 @@ import java.time.LocalDate;
|
||||
* @param arrival 入住日期,酒店本地业务日期
|
||||
* @param departure 离店日期,酒店本地业务日期
|
||||
* @param roomType 目标 Excel 的 Room Type
|
||||
* @param title 目标 Excel 的 Title
|
||||
* @param rateCode 目标 Excel 的 Rate Code
|
||||
* @param adults 目标 Excel 的 Adults;为空时按当前分房人数派生
|
||||
* @param children 目标 Excel 的 Children;为空时默认 0
|
||||
@@ -26,6 +27,7 @@ public record ReservationRoomingListGenerationRequest(
|
||||
LocalDate arrival,
|
||||
LocalDate departure,
|
||||
String roomType,
|
||||
String title,
|
||||
String rateCode,
|
||||
Integer adults,
|
||||
Integer children,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode;
|
||||
import cn.nianxx.thhotel.platform.identity.service.AuthService;
|
||||
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationWorkflowErrorResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
/**
|
||||
* Rooming List 上传接口前置鉴权过滤器。先校验登录和权限,再允许 multipart 参数绑定。
|
||||
*/
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class ReservationRoomingListGenerationAuthorizationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String ENDPOINT = "/api/reservation/rooming-lists/generations";
|
||||
|
||||
private final AuthService authService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 注入认证服务和 JSON 序列化器。
|
||||
*/
|
||||
public ReservationRoomingListGenerationAuthorizationFilter(
|
||||
AuthService authService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.authService = authService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 Rooming List 上传接口提前执行登录和权限校验,避免未登录请求进入 multipart 绑定。
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
if (!matchesRoomingListGeneration(request)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String authorizationHeader = request.getHeader("Authorization");
|
||||
Optional<AuthenticatedUserContext> context = authService.resolveOptionalContext(authorizationHeader);
|
||||
if (context.isEmpty()) {
|
||||
writeError(
|
||||
response,
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
hasBearerToken(authorizationHeader) ? "AUTH_SESSION_INVALID" : "AUTH_TOKEN_REQUIRED",
|
||||
hasBearerToken(authorizationHeader) ? "登录已失效,请重新登录。" : "请先登录后再访问该业务能力。");
|
||||
return;
|
||||
}
|
||||
if (!context.get().permissionCodes()
|
||||
.contains(PlatformPermissionCode.RESERVATION_ROOMING_LIST_GENERATE.name())) {
|
||||
writeError(
|
||||
response,
|
||||
HttpStatus.FORBIDDEN,
|
||||
"FRONTEND_PERMISSION_DENIED",
|
||||
"当前用户没有访问该业务能力的权限。");
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前请求是否为 Rooming List 生成接口。
|
||||
*/
|
||||
private boolean matchesRoomingListGeneration(HttpServletRequest request) {
|
||||
if (!"POST".equalsIgnoreCase(request.getMethod())) {
|
||||
return false;
|
||||
}
|
||||
String contextPath = request.getContextPath();
|
||||
String requestUri = request.getRequestURI();
|
||||
String path = requestUri;
|
||||
if (contextPath != null && !contextPath.isBlank() && requestUri.startsWith(contextPath)) {
|
||||
path = requestUri.substring(contextPath.length());
|
||||
}
|
||||
return ENDPOINT.equals(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入前端业务接口统一错误响应。
|
||||
*/
|
||||
private void writeError(
|
||||
HttpServletResponse response,
|
||||
HttpStatus status,
|
||||
String errorCode,
|
||||
String message) throws IOException {
|
||||
response.setStatus(status.value());
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
objectMapper.writeValue(
|
||||
response.getWriter(),
|
||||
new ReservationWorkflowErrorResponse(errorCode, message, List.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求头中是否携带非空 Bearer token。
|
||||
*/
|
||||
private boolean hasBearerToken(String authorizationHeader) {
|
||||
if (authorizationHeader == null || authorizationHeader.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String prefix = "Bearer ";
|
||||
return authorizationHeader.regionMatches(true, 0, prefix, 0, prefix.length())
|
||||
&& !authorizationHeader.substring(prefix.length()).trim().isBlank();
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ public class ReservationRoomingListGenerationController {
|
||||
@RequestParam("arrival") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate arrival,
|
||||
@RequestParam("departure") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate departure,
|
||||
@RequestParam("room_type") String roomType,
|
||||
@RequestParam(value = "title", required = false) String title,
|
||||
@RequestParam(value = "rate_code", required = false) String rateCode,
|
||||
@RequestParam(value = "adults", required = false) Integer adults,
|
||||
@RequestParam(value = "children", required = false) Integer children,
|
||||
@@ -68,6 +69,7 @@ public class ReservationRoomingListGenerationController {
|
||||
arrival,
|
||||
departure,
|
||||
roomType,
|
||||
title,
|
||||
rateCode,
|
||||
adults,
|
||||
children,
|
||||
|
||||
@@ -113,6 +113,7 @@ public class ReservationRoomingListGenerationServiceImpl implements ReservationR
|
||||
request.arrival(),
|
||||
request.departure(),
|
||||
trimToEmpty(request.roomType()),
|
||||
trimToEmpty(request.title()),
|
||||
trimToEmpty(request.rateCode()),
|
||||
request.adults(),
|
||||
request.children(),
|
||||
|
||||
@@ -90,7 +90,7 @@ public class RoomingListExcelRenderer {
|
||||
row.createCell(0).setCellValue(room.lineNumber());
|
||||
row.createCell(1).setCellValue(room.primaryGuest().lastName());
|
||||
row.createCell(2).setCellValue(room.primaryGuest().firstName());
|
||||
row.createCell(3).setCellValue("");
|
||||
row.createCell(3).setCellValue(defaultString(request.title()));
|
||||
row.createCell(4).setCellValue(request.arrival().format(DateTimeFormatter.ISO_LOCAL_DATE));
|
||||
row.createCell(5).setCellValue(request.departure().format(DateTimeFormatter.ISO_LOCAL_DATE));
|
||||
row.createCell(6).setCellValue(defaultString(request.roomType()));
|
||||
|
||||
@@ -91,6 +91,7 @@ class ReservationRoomingListGenerationControllerTest {
|
||||
.param("arrival", "2026-07-26")
|
||||
.param("departure", "2026-07-29")
|
||||
.param("room_type", "UG1")
|
||||
.param("title", "MR")
|
||||
.param("rate_code", "RACK")
|
||||
.param("payment_type", "CA")
|
||||
.param("nationality", "CN"))
|
||||
@@ -101,6 +102,8 @@ class ReservationRoomingListGenerationControllerTest {
|
||||
.andExpect(header().string("Content-Disposition", containsString("rooming-list-HOTEL-TEST-")))
|
||||
.andExpect(result -> assertThat(result.getResponse().getContentAsByteArray())
|
||||
.startsWith(new byte[]{0x50, 0x4B}))
|
||||
.andExpect(result -> assertThat(titleCell(result.getResponse().getContentAsByteArray()))
|
||||
.isEqualTo("MR"))
|
||||
.andExpect(content().string(not(containsString("P123456"))));
|
||||
}
|
||||
|
||||
@@ -117,6 +120,18 @@ class ReservationRoomingListGenerationControllerTest {
|
||||
.andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRoomingListGenerationWhenTokenMissingBeforeBindingErrors() throws Exception {
|
||||
mockMvc.perform(multipart(ENDPOINT)
|
||||
.file(sourceFile())
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("people_per_room", "2")
|
||||
.param("arrival", "2026-07-26")
|
||||
.param("departure", "2026-07-29"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRoomingListGenerationWhenPermissionMissing() throws Exception {
|
||||
String token = loginToken(mockMvc, "rooming-no-permission", "NoPerm@123456");
|
||||
@@ -199,4 +214,10 @@ class ReservationRoomingListGenerationControllerTest {
|
||||
outputStream.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
private String titleCell(byte[] workbookBytes) throws Exception {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new java.io.ByteArrayInputStream(workbookBytes))) {
|
||||
return workbook.getSheetAt(0).getRow(1).getCell(3).getStringCellValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ class ReservationRoomingListGenerationServiceImplTest {
|
||||
assertThat(cellText(firstRoom.getCell(0))).isEqualTo("1");
|
||||
assertThat(cellText(firstRoom.getCell(1))).isEqualTo("LI");
|
||||
assertThat(cellText(firstRoom.getCell(2))).isEqualTo("CHUNHONG");
|
||||
assertThat(cellText(firstRoom.getCell(3))).isEqualTo("MR");
|
||||
assertThat(cellText(firstRoom.getCell(5))).isEqualTo("2026-07-29");
|
||||
assertThat(cellText(firstRoom.getCell(6))).isEqualTo("UG1");
|
||||
assertThat(cellText(firstRoom.getCell(8))).isEqualTo("1");
|
||||
@@ -121,6 +122,7 @@ class ReservationRoomingListGenerationServiceImplTest {
|
||||
LocalDate.of(2026, 7, 26),
|
||||
LocalDate.of(2026, 7, 29),
|
||||
"UG1",
|
||||
"MR",
|
||||
"RACK",
|
||||
null,
|
||||
0,
|
||||
|
||||
Reference in New Issue
Block a user