兼容第三种房表名单格式

This commit is contained in:
andy committed 2026-08-04 09:41:16 +08:00
1 parent 99fb919158
commit d56587a078
18 files changed
+427 -98

No files matched your search

@@ -7,11 +7,11 @@ import java.time.LocalDate;
*
* @param hotelId 酒店 ID;为空时按当前登录用户默认酒店解析
* @param peoplePerRoom 每间房人数
* @param arrival 第二种来源名单样式使用的入住酒店本地日期;第一种样式可为空并由来源旅游日期派生
* @param departure 第二种来源名单样式使用的离店酒店本地日期;第一种样式可为空并由来源旅游日期派生
* @param arrival 无旅游日期来源名单样式使用的入住酒店本地日期;第一种样式可为空并由来源旅游日期派生
* @param departure 无旅游日期来源名单样式使用的离店酒店本地日期;第一种样式可为空并由来源旅游日期派生
* @param roomType 目标 Excel 的 Room Type
* @param paymentType 目标 Excel 的 Payment Type;默认 BTQR,当前允许 BTQR 或 CA
* @param nationality 目标 Excel 的 Nationality;当前只允许 KR 或 CHN
* @param nationality 目标 Excel 的 Nationality;当前只允许 KR、CN、TH、MM、RS 或 TW
*/
public record ReservationRoomingListGenerationRequest(
String hotelId,
@@ -30,8 +30,8 @@ public class ReservationRoomingListGenerationServiceImpl implements ReservationR
private static final String PAYMENT_TYPE_BTQR = "BTQR";
private static final String PAYMENT_TYPE_CA = "CA";
private static final String DEFAULT_PAYMENT_TYPE = PAYMENT_TYPE_BTQR;
private static final String NATIONALITY_KR = "KR";
private static final String NATIONALITY_CHN = "CHN";
private static final List<String> ALLOWED_NATIONALITIES =
List.of("KR", "CN", "TH", "MM", "RS", "TW");
private final HotelContextService hotelContextService;
private final RoomingListSourceExcelParser sourceExcelParser;
@@ -152,8 +152,8 @@ public class ReservationRoomingListGenerationServiceImpl implements ReservationR
return "";
}
String code = normalized.toUpperCase(java.util.Locale.ROOT);
if (!NATIONALITY_KR.equals(code) && !NATIONALITY_CHN.equals(code)) {
errors.add("nationality: 当前只允许 KR 或 CHN。");
if (!ALLOWED_NATIONALITIES.contains(code)) {
errors.add("nationality: 当前只允许 KR、CN、TH、MM、RS 或 TW。");
}
return code;
}
@@ -8,16 +8,16 @@ import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
/**
* Rooming List 护照姓名解析器。第一版只处理斜杠或空格分隔的英文护照姓名。
* Rooming List 英文姓名解析器。第一版只处理斜杠或空格分隔的英文姓名。
*/
@Component
public class RoomingListNameParser {
/**
* 将来源 Excel 单元格中的护照全名拆分为目标模板 Name / First Name。
* 将来源 Excel 单元格中的英文姓名拆分为目标模板 Name / First Name。
*/
public ReservationRoomingListGuestDto parse(int rowNumber, String rawPassportName) {
String normalized = trimToNull(rawPassportName);
public ReservationRoomingListGuestDto parse(int rowNumber, String rawEnglishName) {
String normalized = trimToNull(rawEnglishName);
if (normalized == null) {
throw invalidName(rowNumber);
}
@@ -35,7 +35,7 @@ public class RoomingListNameParser {
}
/**
* 按 `/` 拆分护照姓名,第一段作为姓氏,剩余部分合并作为名字。
* 按 `/` 拆分英文姓名,第一段作为姓氏,剩余部分合并作为名字。
*/
private ParsedName parseSlashName(String value) {
List<String> parts = Arrays.stream(value.split("/"))
@@ -50,7 +50,7 @@ public class RoomingListNameParser {
}
/**
* 按连续空白拆分护照姓名,第一段作为姓氏,剩余部分合并作为名字。
* 按连续空白拆分英文姓名,第一段作为姓氏,剩余部分合并作为名字。
*/
private ParsedName parseWhitespaceName(String value) {
List<String> parts = Arrays.stream(value.split("\\s+"))
@@ -72,7 +72,7 @@ public class RoomingListNameParser {
HttpStatus.BAD_REQUEST,
"ROOMING_LIST_SOURCE_FILE_INVALID",
"来源名单文件不合法。",
List.of("第 " + rowNumber + " 行护照全名无法拆分为 Name / First Name。"));
List.of("第 " + rowNumber + " 行英文姓名无法拆分为 Name / First Name。"));
}
private boolean isBlank(String value) {
@@ -87,7 +87,7 @@ public class RoomingListNameParser {
}
/**
* 护照姓名拆分结果。
* 英文姓名拆分结果。
*
* @param lastName 姓氏
* @param firstName 名字
@@ -54,7 +54,7 @@ public class RoomingListSourceExcelParser {
}
/**
* 解析来源 Excel。第二种英文姓名来源样式需要调用方提供入住 / 离店酒店本地日期。
* 解析来源 Excel。无旅游日期来源样式需要调用方提供入住 / 离店酒店本地日期。
*/
public ReservationRoomingListSourceDataDto parse(
MultipartFile sourceFile,
@@ -86,7 +86,17 @@ public class RoomingListSourceExcelParser {
formatter,
requireManualStayDateRange(manualArrival, manualDeparture));
} else {
throw invalidFile(List.of("未找到可识别的名单表头:护照全名/旅游日期 或 英文姓/英文名。"));
HeaderLocation singleEnglishNameHeader =
findSingleEnglishNameHeaderIfPresent(sheet, formatter);
if (singleEnglishNameHeader != null) {
sourceRows = parseSingleEnglishNameRows(
sheet,
singleEnglishNameHeader,
formatter,
requireManualStayDateRange(manualArrival, manualDeparture));
} else {
throw invalidFile(List.of("未找到可识别的名单表头:护照全名/旅游日期、英文姓/英文名 或 英文名。"));
}
}
}
return new ReservationRoomingListSourceDataDto(
@@ -209,6 +219,13 @@ public class RoomingListSourceExcelParser {
return null;
}
/**
* 查找第三种来源样式的单列英文名表头;该列值形如 `CAI/HAIYUN`。
*/
private HeaderLocation findSingleEnglishNameHeaderIfPresent(Sheet sheet, DataFormatter formatter) {
return findHeaderIfPresent(sheet, formatter, ENGLISH_FIRST_NAME_HEADER);
}
/**
* 第二种来源样式直接使用英文姓 / 英文名,不读取证件号、中文名或生日等敏感字段。
*/
@@ -243,15 +260,41 @@ public class RoomingListSourceExcelParser {
}
/**
* 第二种来源样式没有可靠旅游日期,必须使用用户提交的入住 / 离店日期。
* 第三种来源样式直接读取单列英文名并复用护照英文名拆分,不读取证件号、生日或联系方式等敏感字段。
*/
private SourceRows parseSingleEnglishNameRows(
Sheet sheet,
HeaderLocation englishNameHeader,
DataFormatter formatter,
TravelDateRange manualStayDateRange) {
List<ReservationRoomingListGuestDto> guests = new ArrayList<>();
for (int rowIndex = englishNameHeader.rowIndex() + 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
continue;
}
String englishName = cellText(row.getCell(englishNameHeader.cellIndex()), formatter);
if (isBlank(englishName)) {
continue;
}
guests.add(nameParser.parse(rowIndex + 1, englishName));
}
if (guests.isEmpty()) {
throw invalidFile(List.of("英文名列没有可用旅客姓名。"));
}
return new SourceRows(List.copyOf(guests), manualStayDateRange);
}
/**
* 无旅游日期来源样式没有可靠旅游日期,必须使用用户提交的入住 / 离店日期。
*/
private TravelDateRange requireManualStayDateRange(LocalDate manualArrival, LocalDate manualDeparture) {
List<String> errors = new ArrayList<>();
if (manualArrival == null) {
errors.add("arrival: 第二种来源名单样式必须填写入住日期。");
errors.add("arrival: 无旅游日期来源名单样式必须填写入住日期。");
}
if (manualDeparture == null) {
errors.add("departure: 第二种来源名单样式必须填写离店日期。");
errors.add("departure: 无旅游日期来源名单样式必须填写离店日期。");
}
if (!errors.isEmpty()) {
throw validationError(errors);