修复Rooming List生成并导入0718需求包

This commit is contained in:
andy
2026-07-18 13:15:35 +07:00
parent 0715e8334f
commit 0e94cc0722
22 changed files with 1367 additions and 3 deletions

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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()));

View File

@@ -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();
}
}
}

View File

@@ -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,