实现 Booking Excel 预处理接入 SuperAgent
This commit is contained in:
@@ -6,6 +6,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.common.dto.SuperAgentDispatchRunDraft;
|
||||
@@ -17,14 +18,30 @@ import cn.nianxx.thhotel.integrations.ai.superagent.common.request.SuperAgentOpe
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentOpenApiResult;
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.repository.SuperAgentDispatchRunRepository;
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.service.SuperAgentOpenApiClient;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectStorageReadRequest;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStorageReadResult;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.ObjectStorageService;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.impl.ObjectStorageException;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageOriginalContent;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageOriginalMediaItem;
|
||||
import cn.nianxx.thhotel.platform.message.repository.SourceMessageInboxRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.excelimport.service.ReservationBookingExcelAttachmentExtractionService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.excelimport.service.impl.BookingExcelExtractionProperties;
|
||||
import cn.nianxx.thhotel.workflows.reservation.excelimport.service.impl.ReservationBookingExcelAttachmentExtractionServiceImpl;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
@@ -153,20 +170,234 @@ class SuperAgentDispatchServiceImplTest {
|
||||
assertThat(updateCaptor.getValue().superagentParsedJson()).isEqualTo("{\"route_code\":\"S10\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAppendBookingExcelExtractionsBeforeDispatchingToSuperAgent() throws Exception {
|
||||
SuperAgentDispatchRunRepository runRepository = mock(SuperAgentDispatchRunRepository.class);
|
||||
SourceMessageInboxRepository sourceMessageRepository = mock(SourceMessageInboxRepository.class);
|
||||
SuperAgentOpenApiClient openApiClient = mock(SuperAgentOpenApiClient.class);
|
||||
ObjectStorageService objectStorageService = mock(ObjectStorageService.class);
|
||||
SuperAgentDispatchProperties properties = properties(true, true);
|
||||
properties.setIncludeBookingExcelExtractions(true);
|
||||
SuperAgentDispatchServiceImpl service = service(
|
||||
runRepository,
|
||||
sourceMessageRepository,
|
||||
openApiClient,
|
||||
objectStorageService,
|
||||
bookingExcelExtractionService(true),
|
||||
properties);
|
||||
SuperAgentDispatchRunSnapshot snapshot = snapshot();
|
||||
byte[] excelBytes = bookingUpdateExcelBytes();
|
||||
when(runRepository.claimDue(any(), any(), any(), any(), any(Integer.class))).thenReturn(List.of(snapshot));
|
||||
when(sourceMessageRepository.findPayloadJson(88001L)).thenReturn(Optional.of("""
|
||||
{
|
||||
"received_at": "2026-07-19T03:00:00Z",
|
||||
"source": {"external_message_id": "mail-agentbus-001"},
|
||||
"body": {"text": "Please check highlighted booking update rows"},
|
||||
"attachments": [
|
||||
{
|
||||
"file_name": "WYNDHAM LIANTAI 2026 UPDATE BOOKING.xlsx",
|
||||
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"external_url": "https://oss.example.test/booking-update.xlsx"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""));
|
||||
when(sourceMessageRepository.findOriginalContent(88001L)).thenReturn(Optional.of(new SourceMessageOriginalContent(
|
||||
88001L,
|
||||
null,
|
||||
null,
|
||||
List.of(new SourceMessageOriginalMediaItem(
|
||||
"ATTACHMENT",
|
||||
"WYNDHAM LIANTAI 2026 UPDATE BOOKING.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
(long) excelBytes.length,
|
||||
"https://oss.example.test/private/booking-update.xlsx?token=download-secret",
|
||||
"media-agentbus-excel-001")))));
|
||||
when(objectStorageService.readObject(any(ObjectStorageReadRequest.class))).thenReturn(new ObjectStorageReadResult(
|
||||
excelBytes,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
(long) excelBytes.length));
|
||||
when(openApiClient.invokeMessage(any(SuperAgentOpenApiMessageRequest.class))).thenReturn(successResult());
|
||||
|
||||
service.processDueDispatches();
|
||||
|
||||
ArgumentCaptor<ObjectStorageReadRequest> storageRequestCaptor =
|
||||
ArgumentCaptor.forClass(ObjectStorageReadRequest.class);
|
||||
verify(objectStorageService).readObject(storageRequestCaptor.capture());
|
||||
assertThat(storageRequestCaptor.getValue().externalUrl())
|
||||
.isEqualTo("https://oss.example.test/private/booking-update.xlsx?token=download-secret");
|
||||
ArgumentCaptor<SuperAgentOpenApiMessageRequest> requestCaptor =
|
||||
ArgumentCaptor.forClass(SuperAgentOpenApiMessageRequest.class);
|
||||
verify(openApiClient).invokeMessage(requestCaptor.capture());
|
||||
assertThat(requestCaptor.getValue().message())
|
||||
.contains("\"attachment_extractions\"")
|
||||
.contains("\"file_type\":\"BOOKING_UPDATE\"")
|
||||
.contains("\"group_code\":\"GRP-2605-001\"")
|
||||
.doesNotContain("download-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotReadAttachmentsWhenBookingExcelDispatchSwitchDisabled() {
|
||||
SuperAgentDispatchRunRepository runRepository = mock(SuperAgentDispatchRunRepository.class);
|
||||
SourceMessageInboxRepository sourceMessageRepository = mock(SourceMessageInboxRepository.class);
|
||||
SuperAgentOpenApiClient openApiClient = mock(SuperAgentOpenApiClient.class);
|
||||
ObjectStorageService objectStorageService = mock(ObjectStorageService.class);
|
||||
SuperAgentDispatchServiceImpl service = service(
|
||||
runRepository,
|
||||
sourceMessageRepository,
|
||||
openApiClient,
|
||||
objectStorageService,
|
||||
bookingExcelExtractionService(true),
|
||||
properties(true, true));
|
||||
SuperAgentDispatchRunSnapshot snapshot = snapshot();
|
||||
when(runRepository.claimDue(any(), any(), any(), any(), any(Integer.class))).thenReturn(List.of(snapshot));
|
||||
when(sourceMessageRepository.findPayloadJson(88001L)).thenReturn(Optional.of("""
|
||||
{
|
||||
"received_at": "2026-07-19T03:00:00Z",
|
||||
"source": {"external_message_id": "mail-agentbus-001"},
|
||||
"attachments": [
|
||||
{"file_name": "WYNDHAM LIANTAI 2026 UPDATE BOOKING.xlsx"}
|
||||
]
|
||||
}
|
||||
"""));
|
||||
when(openApiClient.invokeMessage(any(SuperAgentOpenApiMessageRequest.class))).thenReturn(successResult());
|
||||
|
||||
service.processDueDispatches();
|
||||
|
||||
verify(sourceMessageRepository, never()).findOriginalContent(88001L);
|
||||
verifyNoInteractions(objectStorageService);
|
||||
ArgumentCaptor<SuperAgentOpenApiMessageRequest> requestCaptor =
|
||||
ArgumentCaptor.forClass(SuperAgentOpenApiMessageRequest.class);
|
||||
verify(openApiClient).invokeMessage(requestCaptor.capture());
|
||||
assertThat(requestCaptor.getValue().message()).doesNotContain("\"attachment_extractions\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldContinueDispatchWithSafeWarningWhenBookingExcelAttachmentReadFails() {
|
||||
SuperAgentDispatchRunRepository runRepository = mock(SuperAgentDispatchRunRepository.class);
|
||||
SourceMessageInboxRepository sourceMessageRepository = mock(SourceMessageInboxRepository.class);
|
||||
SuperAgentOpenApiClient openApiClient = mock(SuperAgentOpenApiClient.class);
|
||||
ObjectStorageService objectStorageService = mock(ObjectStorageService.class);
|
||||
SuperAgentDispatchProperties properties = properties(true, true);
|
||||
properties.setIncludeBookingExcelExtractions(true);
|
||||
SuperAgentDispatchServiceImpl service = service(
|
||||
runRepository,
|
||||
sourceMessageRepository,
|
||||
openApiClient,
|
||||
objectStorageService,
|
||||
bookingExcelExtractionService(true),
|
||||
properties);
|
||||
SuperAgentDispatchRunSnapshot snapshot = snapshot();
|
||||
when(runRepository.claimDue(any(), any(), any(), any(), any(Integer.class))).thenReturn(List.of(snapshot));
|
||||
when(sourceMessageRepository.findPayloadJson(88001L)).thenReturn(Optional.of("""
|
||||
{
|
||||
"received_at": "2026-07-19T03:00:00Z",
|
||||
"source": {"external_message_id": "mail-agentbus-001"},
|
||||
"body": {"text": "Please check attachment"}
|
||||
}
|
||||
"""));
|
||||
when(sourceMessageRepository.findOriginalContent(88001L)).thenReturn(Optional.of(new SourceMessageOriginalContent(
|
||||
88001L,
|
||||
null,
|
||||
null,
|
||||
List.of(new SourceMessageOriginalMediaItem(
|
||||
"ATTACHMENT",
|
||||
"WYNDHAM LIANTAI 2026 UPDATE BOOKING.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
2048L,
|
||||
"https://oss.example.test/private/booking-update.xlsx?token=download-secret",
|
||||
"media-agentbus-excel-001")))));
|
||||
when(objectStorageService.readObject(any(ObjectStorageReadRequest.class)))
|
||||
.thenThrow(new ObjectStorageException("对象存储读取失败,token=download-secret。"));
|
||||
when(openApiClient.invokeMessage(any(SuperAgentOpenApiMessageRequest.class))).thenReturn(successResult());
|
||||
|
||||
service.processDueDispatches();
|
||||
|
||||
ArgumentCaptor<SuperAgentOpenApiMessageRequest> requestCaptor =
|
||||
ArgumentCaptor.forClass(SuperAgentOpenApiMessageRequest.class);
|
||||
verify(openApiClient).invokeMessage(requestCaptor.capture());
|
||||
assertThat(requestCaptor.getValue().message())
|
||||
.contains("\"attachment_extractions\"")
|
||||
.contains("ATTACHMENT_DOWNLOAD_FAILED")
|
||||
.doesNotContain("download-secret");
|
||||
verify(runRepository).markSucceeded(any(SuperAgentDispatchRunSuccessUpdate.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldContinueDispatchWhenBookingExcelPayloadEnrichmentFails() {
|
||||
SuperAgentDispatchRunRepository runRepository = mock(SuperAgentDispatchRunRepository.class);
|
||||
SourceMessageInboxRepository sourceMessageRepository = mock(SourceMessageInboxRepository.class);
|
||||
SuperAgentOpenApiClient openApiClient = mock(SuperAgentOpenApiClient.class);
|
||||
SuperAgentDispatchProperties properties = properties(true, true);
|
||||
properties.setIncludeBookingExcelExtractions(true);
|
||||
SuperAgentDispatchServiceImpl service = service(
|
||||
runRepository,
|
||||
sourceMessageRepository,
|
||||
openApiClient,
|
||||
mock(ObjectStorageService.class),
|
||||
bookingExcelExtractionService(true),
|
||||
properties);
|
||||
SuperAgentDispatchRunSnapshot snapshot = snapshot();
|
||||
when(runRepository.claimDue(any(), any(), any(), any(), any(Integer.class))).thenReturn(List.of(snapshot));
|
||||
when(sourceMessageRepository.findPayloadJson(88001L)).thenReturn(Optional.of("""
|
||||
{"received_at":"2026-07-19T03:00:00Z","source":{"external_message_id":"mail-agentbus-001"}}
|
||||
"""));
|
||||
when(sourceMessageRepository.findOriginalContent(88001L))
|
||||
.thenThrow(new RuntimeException("unexpected token=download-secret"));
|
||||
when(openApiClient.invokeMessage(any(SuperAgentOpenApiMessageRequest.class))).thenReturn(successResult());
|
||||
|
||||
service.processDueDispatches();
|
||||
|
||||
ArgumentCaptor<SuperAgentOpenApiMessageRequest> requestCaptor =
|
||||
ArgumentCaptor.forClass(SuperAgentOpenApiMessageRequest.class);
|
||||
verify(openApiClient).invokeMessage(requestCaptor.capture());
|
||||
assertThat(requestCaptor.getValue().message())
|
||||
.doesNotContain("\"attachment_extractions\"")
|
||||
.doesNotContain("download-secret");
|
||||
verify(runRepository).markSucceeded(any(SuperAgentDispatchRunSuccessUpdate.class));
|
||||
}
|
||||
|
||||
private SuperAgentDispatchServiceImpl service(
|
||||
SuperAgentDispatchRunRepository runRepository,
|
||||
SourceMessageInboxRepository sourceMessageRepository,
|
||||
SuperAgentOpenApiClient openApiClient,
|
||||
SuperAgentDispatchProperties properties) {
|
||||
return service(
|
||||
runRepository,
|
||||
sourceMessageRepository,
|
||||
openApiClient,
|
||||
mock(ObjectStorageService.class),
|
||||
bookingExcelExtractionService(false),
|
||||
properties);
|
||||
}
|
||||
|
||||
private SuperAgentDispatchServiceImpl service(
|
||||
SuperAgentDispatchRunRepository runRepository,
|
||||
SourceMessageInboxRepository sourceMessageRepository,
|
||||
SuperAgentOpenApiClient openApiClient,
|
||||
ObjectStorageService objectStorageService,
|
||||
ReservationBookingExcelAttachmentExtractionService bookingExcelExtractionService,
|
||||
SuperAgentDispatchProperties properties) {
|
||||
return new SuperAgentDispatchServiceImpl(
|
||||
properties,
|
||||
new SuperAgentOpenApiProperties(),
|
||||
runRepository,
|
||||
sourceMessageRepository,
|
||||
openApiClient,
|
||||
objectStorageService,
|
||||
bookingExcelExtractionService,
|
||||
new ObjectMapper());
|
||||
}
|
||||
|
||||
private ReservationBookingExcelAttachmentExtractionService bookingExcelExtractionService(boolean enabled) {
|
||||
BookingExcelExtractionProperties properties = new BookingExcelExtractionProperties();
|
||||
properties.setEnabled(enabled);
|
||||
properties.setLookbackMonths(6);
|
||||
properties.setMaxSelectedMonths(3);
|
||||
properties.setMaxFileSizeBytes(2 * 1024 * 1024);
|
||||
return new ReservationBookingExcelAttachmentExtractionServiceImpl(properties);
|
||||
}
|
||||
|
||||
private SuperAgentDispatchProperties properties(boolean enabled, boolean workerEnabled) {
|
||||
SuperAgentDispatchProperties properties = new SuperAgentDispatchProperties();
|
||||
properties.setEnabled(enabled);
|
||||
@@ -224,4 +455,51 @@ class SuperAgentDispatchServiceImplTest {
|
||||
now,
|
||||
now);
|
||||
}
|
||||
|
||||
private SuperAgentOpenApiResult successResult() {
|
||||
return new SuperAgentOpenApiResult(
|
||||
"session-dispatch-001",
|
||||
"run-dispatch-001",
|
||||
"profile-dispatch",
|
||||
"version-dispatch",
|
||||
"model-dispatch",
|
||||
"{\"route_code\":\"S10\"}",
|
||||
10,
|
||||
5,
|
||||
15,
|
||||
List.of("trace", "end"),
|
||||
List.of(),
|
||||
"/api/open/agent-sessions/session-dispatch-001/runs/run-dispatch-001",
|
||||
"4");
|
||||
}
|
||||
|
||||
private byte[] bookingUpdateExcelBytes() throws Exception {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = workbook.createSheet("BOOKING 05-2026");
|
||||
Row header = sheet.createRow(0);
|
||||
List<String> headers = List.of("Group Code", "Hotel", "Check In", "Check Out", "Room Type", "Rooms", "Remark");
|
||||
for (int index = 0; index < headers.size(); index++) {
|
||||
header.createCell(index).setCellValue(headers.get(index));
|
||||
}
|
||||
Row row = sheet.createRow(1);
|
||||
row.createCell(0).setCellValue("GRP-2605-001");
|
||||
row.createCell(1).setCellValue("Wyndham Liantai");
|
||||
row.createCell(2).setCellValue("2026-05-01");
|
||||
row.createCell(3).setCellValue("2026-05-03");
|
||||
row.createCell(4).setCellValue("UG1");
|
||||
row.createCell(5).setCellValue("2");
|
||||
row.createCell(6).setCellValue("Need update");
|
||||
row.getCell(6).setCellStyle(yellowFill(workbook));
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private XSSFCellStyle yellowFill(XSSFWorkbook workbook) {
|
||||
XSSFCellStyle style = workbook.createCellStyle();
|
||||
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
style.setFillForegroundColor(IndexedColors.YELLOW.getIndex());
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package cn.nianxx.thhotel.integrations.storage.aliyunoss.service.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectStorageReadRequest;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStorageReadResult;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Authenticator;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpHeaders;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AliyunOssObjectStorageServiceImplTest {
|
||||
|
||||
@Test
|
||||
void shouldReadObjectContentFromSavedUrl() {
|
||||
TrackingInputStream responseBody = new TrackingInputStream("excel-bytes".getBytes(StandardCharsets.UTF_8));
|
||||
AliyunOssObjectStorageServiceImpl service = new AliyunOssObjectStorageServiceImpl(
|
||||
new AliyunOssProperties(),
|
||||
new FakeHttpClient(200, responseBody));
|
||||
|
||||
ObjectStorageReadResult result = service.readObject(new ObjectStorageReadRequest(
|
||||
"https://oss.example.test/private/booking.xlsx?token=download-secret",
|
||||
1024L));
|
||||
|
||||
assertThat(result.content()).isEqualTo("excel-bytes".getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(result.contentType()).isEqualTo("application/vnd.ms-excel");
|
||||
assertThat(result.sizeBytes()).isEqualTo(11L);
|
||||
assertThat(responseBody.closed()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectObjectWhenContentExceedsLimit() {
|
||||
TrackingInputStream responseBody = new TrackingInputStream("too-large".getBytes(StandardCharsets.UTF_8));
|
||||
AliyunOssObjectStorageServiceImpl service = new AliyunOssObjectStorageServiceImpl(
|
||||
new AliyunOssProperties(),
|
||||
new FakeHttpClient(200, responseBody));
|
||||
|
||||
assertThatThrownBy(() -> service.readObject(new ObjectStorageReadRequest(
|
||||
"https://oss.example.test/private/too-large.xlsx?token=download-secret",
|
||||
4L)))
|
||||
.isInstanceOf(ObjectStorageException.class)
|
||||
.hasMessageContaining("对象存储读取内容超过大小限制")
|
||||
.hasMessageNotContaining("download-secret");
|
||||
assertThat(responseBody.closed()).isTrue();
|
||||
}
|
||||
|
||||
private static final class TrackingInputStream extends ByteArrayInputStream {
|
||||
private boolean closed;
|
||||
|
||||
private TrackingInputStream(byte[] buffer) {
|
||||
super(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
closed = true;
|
||||
super.close();
|
||||
}
|
||||
|
||||
private boolean closed() {
|
||||
return closed;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FakeHttpClient extends HttpClient {
|
||||
private final int statusCode;
|
||||
private final TrackingInputStream responseBody;
|
||||
|
||||
private FakeHttpClient(int statusCode, TrackingInputStream responseBody) {
|
||||
this.statusCode = statusCode;
|
||||
this.responseBody = responseBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CookieHandler> cookieHandler() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Duration> connectTimeout() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Redirect followRedirects() {
|
||||
return Redirect.NEVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProxySelector> proxy() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLContext sslContext() {
|
||||
try {
|
||||
return SSLContext.getDefault();
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLParameters sslParameters() {
|
||||
return new SSLParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Authenticator> authenticator() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return Version.HTTP_1_1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Executor> executor() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
|
||||
return new SimpleHttpResponse<>(request, statusCode, (T) responseBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(
|
||||
HttpRequest request,
|
||||
HttpResponse.BodyHandler<T> responseBodyHandler) {
|
||||
return CompletableFuture.failedFuture(new UnsupportedOperationException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(
|
||||
HttpRequest request,
|
||||
HttpResponse.BodyHandler<T> responseBodyHandler,
|
||||
HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
|
||||
return CompletableFuture.failedFuture(new UnsupportedOperationException());
|
||||
}
|
||||
}
|
||||
|
||||
private record SimpleHttpResponse<T>(
|
||||
HttpRequest request,
|
||||
int statusCode,
|
||||
T body
|
||||
) implements HttpResponse<T> {
|
||||
|
||||
@Override
|
||||
public Optional<HttpResponse<T>> previousResponse() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders headers() {
|
||||
return HttpHeaders.of(
|
||||
Map.of("Content-Type", List.of("application/vnd.ms-excel")),
|
||||
(name, value) -> true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<SSLSession> sslSession() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.URI uri() {
|
||||
return request.uri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpClient.Version version() {
|
||||
return HttpClient.Version.HTTP_1_1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,13 +26,21 @@ import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectSto
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStoragePutResult;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.ObjectStorageService;
|
||||
import cn.nianxx.thhotel.platform.debug.service.DebugEmlSuperAgentRunService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.excelimport.service.impl.BookingExcelExtractionProperties;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -57,7 +65,9 @@ import org.springframework.web.client.RestClientResponseException;
|
||||
"debug.eml-upload.access-key=test-debug-upload-key",
|
||||
"debug.eml-upload.max-file-bytes=1048576",
|
||||
"debug.eml-upload.sse-heartbeat-interval=25ms",
|
||||
"debug.eml-upload.include-booking-excel-extractions=true",
|
||||
"aliyun.oss.debug-eml-prefix=debug/eml/",
|
||||
"reservation.booking-excel-extraction.enabled=true",
|
||||
"superagent.open-api.enabled=true",
|
||||
"superagent.open-api.external-subject-id=test-debug-eml"
|
||||
})
|
||||
@@ -77,6 +87,9 @@ class DebugEmlSuperAgentControllerTest {
|
||||
@Autowired
|
||||
private DebugEmlSuperAgentRunService runService;
|
||||
|
||||
@Autowired
|
||||
private BookingExcelExtractionProperties bookingExcelExtractionProperties;
|
||||
|
||||
@MockBean
|
||||
private ObjectStorageService objectStorageService;
|
||||
|
||||
@@ -259,6 +272,106 @@ class DebugEmlSuperAgentControllerTest {
|
||||
.andExpect(content().string(not(containsString("test-debug-upload-key"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAttachBookingExcelExtractionsToDebugPayloadAndSuperAgentMessage() throws Exception {
|
||||
when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
|
||||
ObjectStoragePutRequest request = invocation.getArgument(0);
|
||||
return new ObjectStoragePutResult(
|
||||
request.objectKey(),
|
||||
"https://oss.example.test/" + request.objectKey(),
|
||||
request.contentType(),
|
||||
request.sizeBytes());
|
||||
});
|
||||
when(superAgentOpenApiClient.invokeMailDebug(any())).thenReturn(new SuperAgentOpenApiResult(
|
||||
"session-debug-excel",
|
||||
"run-debug-excel",
|
||||
"profile-debug",
|
||||
"profile-version-debug",
|
||||
"debug-model",
|
||||
"{\"route_code\":\"S10\"}",
|
||||
11,
|
||||
7,
|
||||
18,
|
||||
List.of("metadata", "values", "end")));
|
||||
|
||||
mockMvc.perform(multipart(ENDPOINT)
|
||||
.file(emlFileWithBookingExcelAttachment())
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("run_label", "controller-excel-extraction")
|
||||
.header("X-TH-Hotel-Debug-Upload-Key", "test-debug-upload-key"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.attachment_extractions[0].file_type").value("BOOKING_UPDATE"))
|
||||
.andExpect(jsonPath("$.attachment_extractions[0].month_filter.base_month").value("2026-07"))
|
||||
.andExpect(jsonPath("$.attachment_extractions[0].month_filter.selected_months[0]").value("2026-05"))
|
||||
.andExpect(jsonPath("$.attachment_extractions[0].sheets[0].highlighted_rows[0].row.group_code")
|
||||
.value("GRP-2605-001"))
|
||||
.andExpect(jsonPath("$.agentbus_like_payload.attachment_extractions[0].file_type")
|
||||
.value("BOOKING_UPDATE"))
|
||||
.andExpect(jsonPath("$.agentbus_like_payload.attachment_extractions[0].sheets[0]"
|
||||
+ ".highlighted_rows[0].highlight_cells[0].header").value("Remark"));
|
||||
|
||||
ArgumentCaptor<SuperAgentMailDebugRequest> superAgentRequestCaptor =
|
||||
ArgumentCaptor.forClass(SuperAgentMailDebugRequest.class);
|
||||
verify(superAgentOpenApiClient).invokeMailDebug(superAgentRequestCaptor.capture());
|
||||
org.assertj.core.api.Assertions.assertThat(superAgentRequestCaptor.getValue().message())
|
||||
.contains("\"attachment_extractions\"")
|
||||
.contains("\"file_type\":\"BOOKING_UPDATE\"")
|
||||
.contains("\"group_code\":\"GRP-2605-001\"")
|
||||
.doesNotContain("test-debug-upload-key");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldQueryBookingExcelExtractionsFromPersistedDebugPayload() throws Exception {
|
||||
mockStorageAndSuperAgentSuccess();
|
||||
|
||||
mockMvc.perform(multipart(ENDPOINT)
|
||||
.file(emlFileWithBookingExcelAttachment())
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("run_label", "query-excel-extraction")
|
||||
.header("X-TH-Hotel-Debug-Upload-Key", "test-debug-upload-key"))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
Long runId = jdbcTemplate.queryForObject("""
|
||||
SELECT id
|
||||
FROM platform_debug_eml_superagent_run
|
||||
WHERE run_label = 'query-excel-extraction'
|
||||
""", Long.class);
|
||||
|
||||
mockMvc.perform(get(ENDPOINT + "/" + runId)
|
||||
.header("X-TH-Hotel-Debug-Upload-Key", "test-debug-upload-key"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.attachment_extractions[0].file_type").value("BOOKING_UPDATE"))
|
||||
.andExpect(jsonPath("$.attachment_extractions[0].month_filter.selected_months[0]").value("2026-05"))
|
||||
.andExpect(jsonPath("$.attachment_extractions[0].sheets[0].highlighted_rows[0].row.group_code")
|
||||
.value("GRP-2605-001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotAttachBookingExcelExtractionsWhenGlobalExtractionDisabled() throws Exception {
|
||||
bookingExcelExtractionProperties.setEnabled(false);
|
||||
try {
|
||||
mockStorageAndSuperAgentSuccess();
|
||||
|
||||
mockMvc.perform(multipart(ENDPOINT)
|
||||
.file(emlFileWithBookingExcelAttachment())
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("run_label", "controller-excel-extraction-disabled")
|
||||
.header("X-TH-Hotel-Debug-Upload-Key", "test-debug-upload-key"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.attachment_extractions").doesNotExist())
|
||||
.andExpect(jsonPath("$.agentbus_like_payload.attachment_extractions").doesNotExist());
|
||||
|
||||
ArgumentCaptor<SuperAgentMailDebugRequest> superAgentRequestCaptor =
|
||||
ArgumentCaptor.forClass(SuperAgentMailDebugRequest.class);
|
||||
verify(superAgentOpenApiClient).invokeMailDebug(superAgentRequestCaptor.capture());
|
||||
org.assertj.core.api.Assertions.assertThat(superAgentRequestCaptor.getValue().message())
|
||||
.doesNotContain("\"attachment_extractions\"")
|
||||
.doesNotContain("BOOKING_EXCEL_EXTRACTION_DISABLED");
|
||||
} finally {
|
||||
bookingExcelExtractionProperties.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStreamDebugStagesSuperAgentTraceAndFinalResult() throws Exception {
|
||||
when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
|
||||
@@ -716,6 +829,14 @@ class DebugEmlSuperAgentControllerTest {
|
||||
unsafeHtmlEmlBytes());
|
||||
}
|
||||
|
||||
private MockMultipartFile emlFileWithBookingExcelAttachment() throws Exception {
|
||||
return new MockMultipartFile(
|
||||
"file",
|
||||
"debug-booking-with-excel.eml",
|
||||
MediaType.TEXT_PLAIN_VALUE,
|
||||
emlBytesWithBookingExcelAttachment());
|
||||
}
|
||||
|
||||
private static final class FailingOutputStream extends OutputStream {
|
||||
|
||||
@Override
|
||||
@@ -802,4 +923,61 @@ class DebugEmlSuperAgentControllerTest {
|
||||
--rel-boundary--
|
||||
""".replace("\n", "\r\n").getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private byte[] emlBytesWithBookingExcelAttachment() throws Exception {
|
||||
String attachmentBase64 = Base64.getMimeEncoder(76, "\r\n".getBytes(StandardCharsets.UTF_8))
|
||||
.encodeToString(bookingUpdateExcelBytes());
|
||||
return ("""
|
||||
From: Guest <guest@example.test>
|
||||
To: Reservations <reservations@example.test>
|
||||
Subject: Debug Booking With Excel
|
||||
Date: Thu, 09 Jul 2026 01:30:00 +0000
|
||||
Message-ID: <debug-controller-excel-message-001@example.test>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary="mixed-boundary"
|
||||
|
||||
--mixed-boundary
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
Please check highlighted booking update rows.
|
||||
|
||||
--mixed-boundary
|
||||
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; name="WYNDHAM LIANTAI 2026 UPDATE BOOKING.xlsx"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename="WYNDHAM LIANTAI 2026 UPDATE BOOKING.xlsx"
|
||||
|
||||
%s
|
||||
--mixed-boundary--
|
||||
""".formatted(attachmentBase64)).replace("\n", "\r\n").getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private byte[] bookingUpdateExcelBytes() throws Exception {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = workbook.createSheet("BOOKING 05-2026");
|
||||
Row header = sheet.createRow(0);
|
||||
List<String> headers = List.of("Group Code", "Hotel", "Check In", "Check Out", "Room Type", "Rooms", "Remark");
|
||||
for (int index = 0; index < headers.size(); index++) {
|
||||
header.createCell(index).setCellValue(headers.get(index));
|
||||
}
|
||||
Row row = sheet.createRow(1);
|
||||
row.createCell(0).setCellValue("GRP-2605-001");
|
||||
row.createCell(1).setCellValue("Wyndham Liantai");
|
||||
row.createCell(2).setCellValue("2026-05-01");
|
||||
row.createCell(3).setCellValue("2026-05-03");
|
||||
row.createCell(4).setCellValue("UG1");
|
||||
row.createCell(5).setCellValue("2");
|
||||
row.createCell(6).setCellValue("Need update");
|
||||
row.getCell(6).setCellStyle(yellowFill(workbook));
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private XSSFCellStyle yellowFill(XSSFWorkbook workbook) {
|
||||
XSSFCellStyle style = workbook.createCellStyle();
|
||||
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
style.setFillForegroundColor(IndexedColors.YELLOW.getIndex());
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.excelimport.service.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.excelimport.common.enums.BookingExcelFileType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.excelimport.common.request.BookingExcelAttachmentExtractionRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.excelimport.common.result.BookingExcelAttachmentExtractionResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.excelimport.common.result.BookingExcelHighlightedRowResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.excelimport.service.ReservationBookingExcelAttachmentExtractionService;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
class ReservationBookingExcelAttachmentExtractionServiceImplTest {
|
||||
|
||||
private ReservationBookingExcelAttachmentExtractionService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
BookingExcelExtractionProperties properties = new BookingExcelExtractionProperties();
|
||||
properties.setEnabled(true);
|
||||
properties.setLookbackMonths(6);
|
||||
properties.setMaxSelectedMonths(3);
|
||||
properties.setMaxFileSizeBytes(2 * 1024 * 1024);
|
||||
properties.setMaxSheets(24);
|
||||
properties.setMaxRowsPerSheet(2000);
|
||||
service = new ReservationBookingExcelAttachmentExtractionServiceImpl(properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExcludePassengerRosterWorkbookByHeaderCombination() throws Exception {
|
||||
BookingExcelAttachmentExtractionResult result = service.extract(request(
|
||||
"LLTNamLis.xlsx",
|
||||
passengerRosterWorkbook(),
|
||||
Instant.parse("2026-07-19T03:00:00Z")));
|
||||
|
||||
assertThat(result.fileType()).isEqualTo(BookingExcelFileType.PASSENGER_ROSTER);
|
||||
assertThat(result.excluded()).isTrue();
|
||||
assertThat(result.skippedReason()).isEqualTo("PASSENGER_ROSTER");
|
||||
assertThat(result.sheets()).isEmpty();
|
||||
assertThat(result.warnings()).contains("PASSENGER_ROSTER_EXCLUDED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSelectLatestThreeAvailableMonthsWithinSixMonthLookback() throws Exception {
|
||||
BookingExcelAttachmentExtractionResult result = service.extract(request(
|
||||
"WYNDHAM LIANTAI 2026 UPDATE BOOKING.xlsx",
|
||||
bookingUpdateWorkbookWithMonths("BOOKING 03-2026", "BOOKING 04-2026", "BOOKING 05-2026", "BOOKING 06-2026"),
|
||||
Instant.parse("2026-07-19T03:00:00Z")));
|
||||
|
||||
assertThat(result.fileType()).isEqualTo(BookingExcelFileType.BOOKING_UPDATE);
|
||||
assertThat(result.excluded()).isFalse();
|
||||
assertThat(result.monthFilter().baseMonth()).isEqualTo("2026-07");
|
||||
assertThat(result.monthFilter().candidateFromMonth()).isEqualTo("2026-02");
|
||||
assertThat(result.monthFilter().candidateToMonth()).isEqualTo("2026-07");
|
||||
assertThat(result.monthFilter().availableMonths())
|
||||
.containsExactly("2026-03", "2026-04", "2026-05", "2026-06");
|
||||
assertThat(result.monthFilter().selectedMonths()).containsExactly("2026-04", "2026-05", "2026-06");
|
||||
assertThat(result.monthFilter().matchedSheets())
|
||||
.containsExactly("BOOKING 04-2026", "BOOKING 05-2026", "BOOKING 06-2026");
|
||||
assertThat(result.monthFilter().skippedSheets()).contains("BOOKING 03-2026");
|
||||
assertThat(result.sheets()).extracting(sheet -> sheet.sheetName())
|
||||
.containsExactly("BOOKING 04-2026", "BOOKING 05-2026", "BOOKING 06-2026");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExtractRowsWhenAnyBusinessColumnHasBackgroundFill() throws Exception {
|
||||
BookingExcelAttachmentExtractionResult result = service.extract(request(
|
||||
"春节-附加费用xlsx.xlsx",
|
||||
surchargeWorkbookWithHighlightedRows(),
|
||||
Instant.parse("2026-07-19T03:00:00Z")));
|
||||
|
||||
assertThat(result.fileType()).isEqualTo(BookingExcelFileType.BOOKING_SURCHARGE);
|
||||
assertThat(result.sheets()).hasSize(1);
|
||||
assertThat(result.sheets().get(0).highlightedRows()).hasSize(1);
|
||||
BookingExcelHighlightedRowResult highlightedRow = result.sheets().get(0).highlightedRows().get(0);
|
||||
assertThat(highlightedRow.rowNumber()).isEqualTo(2);
|
||||
assertThat(highlightedRow.row()).containsEntry("group_code", "GRP-2605-001");
|
||||
assertThat(highlightedRow.row()).containsEntry("hotel", "Wyndham Liantai");
|
||||
assertThat(highlightedRow.row()).containsEntry("remark", "春节附加费待确认");
|
||||
assertThat(highlightedRow.highlightColors()).contains("FFFFFF00");
|
||||
assertThat(highlightedRow.highlightCells()).hasSize(1);
|
||||
assertThat(highlightedRow.highlightCells().get(0).header()).isEqualTo("Remark");
|
||||
assertThat(highlightedRow.rawRow()).containsEntry("A", "GRP-2605-001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWarnWhenAvailableMonthsLessThanLimit() throws Exception {
|
||||
BookingExcelAttachmentExtractionResult result = service.extract(request(
|
||||
"WYNDHAM LIANTAI 2026 UPDATE BOOKING.xlsx",
|
||||
bookingUpdateWorkbookWithMonths("BOOKING 03-2026", "BOOKING 04-2026"),
|
||||
Instant.parse("2026-07-19T03:00:00Z")));
|
||||
|
||||
assertThat(result.monthFilter().selectedMonths()).containsExactly("2026-03", "2026-04");
|
||||
assertThat(result.warnings()).contains("MATCHED_MONTHS_LESS_THAN_LIMIT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHonorConfiguredMaxFileSizeDataSize() throws Exception {
|
||||
BookingExcelExtractionProperties properties = new BookingExcelExtractionProperties();
|
||||
properties.setEnabled(true);
|
||||
properties.setMaxFileSize(DataSize.ofBytes(1));
|
||||
ReservationBookingExcelAttachmentExtractionService localService =
|
||||
new ReservationBookingExcelAttachmentExtractionServiceImpl(properties);
|
||||
BookingExcelAttachmentExtractionResult result = localService.extract(request(
|
||||
"WYNDHAM LIANTAI 2026 UPDATE BOOKING.xlsx",
|
||||
bookingUpdateWorkbookWithMonths("BOOKING 05-2026"),
|
||||
Instant.parse("2026-07-19T03:00:00Z")));
|
||||
|
||||
assertThat(result.excluded()).isTrue();
|
||||
assertThat(result.skippedReason()).isEqualTo("EXCEL_ATTACHMENT_TOO_LARGE");
|
||||
assertThat(result.warnings()).contains("EXCEL_ATTACHMENT_TOO_LARGE");
|
||||
}
|
||||
|
||||
private BookingExcelAttachmentExtractionRequest request(
|
||||
String fileName,
|
||||
byte[] content,
|
||||
Instant baseInstant) {
|
||||
return new BookingExcelAttachmentExtractionRequest(
|
||||
fileName,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
(long) content.length,
|
||||
content,
|
||||
baseInstant,
|
||||
ZoneId.of("Asia/Bangkok"),
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
private byte[] passengerRosterWorkbook() throws Exception {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = workbook.createSheet("Sheet1");
|
||||
Row header = sheet.createRow(0);
|
||||
List<String> headers = List.of(
|
||||
"旅游批次", "旅游日期", "团号", "姓名", "护照全名", "证件号", "性别", "生日");
|
||||
for (int index = 0; index < headers.size(); index++) {
|
||||
header.createCell(index).setCellValue(headers.get(index));
|
||||
}
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] bookingUpdateWorkbookWithMonths(String... sheetNames) throws Exception {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
XSSFCellStyle highlightStyle = yellowFill(workbook);
|
||||
for (String sheetName : sheetNames) {
|
||||
Sheet sheet = workbook.createSheet(sheetName);
|
||||
createBookingHeader(sheet);
|
||||
Row dataRow = sheet.createRow(1);
|
||||
dataRow.createCell(0).setCellValue("GRP-" + sheetName.substring(sheetName.length() - 7));
|
||||
dataRow.createCell(1).setCellValue("Wyndham Liantai");
|
||||
dataRow.createCell(2).setCellValue("2026-05-01");
|
||||
dataRow.createCell(3).setCellValue("2026-05-03");
|
||||
dataRow.createCell(4).setCellValue("UG1");
|
||||
dataRow.createCell(5).setCellValue("2");
|
||||
dataRow.createCell(6).setCellValue("Need update");
|
||||
dataRow.getCell(6).setCellStyle(highlightStyle);
|
||||
}
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] surchargeWorkbookWithHighlightedRows() throws Exception {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = workbook.createSheet("BOOKING 05-2026");
|
||||
createBookingHeader(sheet);
|
||||
XSSFCellStyle headerStyle = yellowFill(workbook);
|
||||
sheet.getRow(0).forEach(cell -> cell.setCellStyle(headerStyle));
|
||||
|
||||
Row highlighted = sheet.createRow(1);
|
||||
highlighted.createCell(0).setCellValue("GRP-2605-001");
|
||||
highlighted.createCell(1).setCellValue("Wyndham Liantai");
|
||||
highlighted.createCell(2).setCellValue("2026-05-01");
|
||||
highlighted.createCell(3).setCellValue("2026-05-03");
|
||||
highlighted.createCell(4).setCellValue("UG1");
|
||||
highlighted.createCell(5).setCellValue("2");
|
||||
highlighted.createCell(6).setCellValue("春节附加费待确认");
|
||||
highlighted.getCell(6).setCellStyle(yellowFill(workbook));
|
||||
|
||||
Row normal = sheet.createRow(2);
|
||||
normal.createCell(0).setCellValue("GRP-2605-002");
|
||||
normal.createCell(1).setCellValue("Wyndham Liantai");
|
||||
normal.createCell(2).setCellValue("2026-05-05");
|
||||
normal.createCell(3).setCellValue("2026-05-07");
|
||||
normal.createCell(4).setCellValue("RM1");
|
||||
normal.createCell(5).setCellValue("1");
|
||||
normal.createCell(6).setCellValue("无需处理");
|
||||
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void createBookingHeader(Sheet sheet) {
|
||||
Row header = sheet.createRow(0);
|
||||
List<String> headers = List.of("Group Code", "Hotel", "Check In", "Check Out", "Room Type", "Rooms", "Remark");
|
||||
for (int index = 0; index < headers.size(); index++) {
|
||||
header.createCell(index).setCellValue(headers.get(index));
|
||||
}
|
||||
}
|
||||
|
||||
private XSSFCellStyle yellowFill(XSSFWorkbook workbook) {
|
||||
XSSFCellStyle style = workbook.createCellStyle();
|
||||
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
style.setFillForegroundColor(IndexedColors.YELLOW.getIndex());
|
||||
return style;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user