实现V4数据库目录与Lookup接口
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.dto;
|
||||
|
||||
/**
|
||||
* Reservation V4 第一版 Account 目录项。当前为后端固定种子,后续可替换为酒店目录表。
|
||||
* Reservation V4 Account 目录项。由当前酒店数据库目录快照转换而来。
|
||||
*
|
||||
* @param accountCode Account 稳定代码,SuperAgent 与前端提交均使用该值
|
||||
* @param accountName Account 显示名称
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Reservation V4 Account 目录快照。Repository 输出快照,避免 Service 直接依赖 Entity。
|
||||
*
|
||||
* @param id 目录记录 ID
|
||||
* @param hotelId 酒店 ID
|
||||
* @param accountCode Account 稳定代码
|
||||
* @param accountName Account 显示名称
|
||||
* @param marketCode 关联 Market 代码
|
||||
* @param sourceCode 关联 Source 代码
|
||||
* @param status 目录状态
|
||||
* @param sourceSystem 目录来源
|
||||
* @param catalogVersion 目录版本
|
||||
* @param lastSyncedAt 最近同步 UTC 时间
|
||||
* @param metadataJson 扩展属性 JSON
|
||||
*/
|
||||
public record ReservationV4CatalogAccountSnapshot(
|
||||
Long id,
|
||||
String hotelId,
|
||||
String accountCode,
|
||||
String accountName,
|
||||
String marketCode,
|
||||
String sourceCode,
|
||||
String status,
|
||||
String sourceSystem,
|
||||
String catalogVersion,
|
||||
LocalDateTime lastSyncedAt,
|
||||
String metadataJson
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Reservation V4 通用代码目录快照。覆盖 Room Type、Rate Code、Market、Source。
|
||||
*
|
||||
* @param id 目录记录 ID
|
||||
* @param hotelId 酒店 ID
|
||||
* @param catalogType 目录类型
|
||||
* @param code 稳定代码
|
||||
* @param displayName 显示名称
|
||||
* @param status 目录状态
|
||||
* @param sourceSystem 目录来源
|
||||
* @param sortOrder 展示排序
|
||||
* @param catalogVersion 目录版本
|
||||
* @param lastSyncedAt 最近同步 UTC 时间
|
||||
* @param metadataJson 扩展属性 JSON
|
||||
*/
|
||||
public record ReservationV4CatalogCodeSnapshot(
|
||||
Long id,
|
||||
String hotelId,
|
||||
String catalogType,
|
||||
String code,
|
||||
String displayName,
|
||||
String status,
|
||||
String sourceSystem,
|
||||
Integer sortOrder,
|
||||
String catalogVersion,
|
||||
LocalDateTime lastSyncedAt,
|
||||
String metadataJson
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.enums;
|
||||
|
||||
/**
|
||||
* Reservation V4 目录来源稳定代码。前端可用该值判断目录是否来自固定初始化、后台维护或未来 PMS 同步。
|
||||
*/
|
||||
public enum ReservationV4CatalogSourceSystem {
|
||||
FIXED_SEED_IMPORT,
|
||||
SYSTEM_MANAGED,
|
||||
PMS_SYNC
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.enums;
|
||||
|
||||
/**
|
||||
* Reservation V4 目录状态。第一版 lookup 和字段校验只接受 ACTIVE 目录。
|
||||
*/
|
||||
public enum ReservationV4CatalogStatus {
|
||||
ACTIVE,
|
||||
DISABLED
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.enums;
|
||||
|
||||
/**
|
||||
* Reservation V4 目录类型。Account 单独成表,其余稳定代码放在通用目录表。
|
||||
*/
|
||||
public enum ReservationV4CatalogType {
|
||||
ACCOUNT,
|
||||
ROOM_TYPE,
|
||||
RATE_CODE,
|
||||
MARKET,
|
||||
SOURCE
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.request;
|
||||
|
||||
/**
|
||||
* Reservation V4 目录 lookup 查询请求。Controller 负责承接 HTTP 参数,Service 负责酒店隔离和分页默认值。
|
||||
*
|
||||
* @param hotelId 请求酒店 ID,未传时使用当前登录用户默认酒店
|
||||
* @param catalogType 目录类型
|
||||
* @param keyword 代码或名称关键字
|
||||
* @param pageNum 页码,从 1 开始
|
||||
* @param pageSize 每页数量
|
||||
*/
|
||||
public record ReservationV4CatalogLookupRequest(
|
||||
String hotelId,
|
||||
String catalogType,
|
||||
String keyword,
|
||||
Integer pageNum,
|
||||
Integer pageSize
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Reservation V4 lookup 单条目录返回项。不同目录类型共享该结构,未适用字段返回 null。
|
||||
*
|
||||
* @param code 目录稳定代码
|
||||
* @param displayName 目录显示名称
|
||||
* @param status 目录状态
|
||||
* @param catalogSource 单条目录来源
|
||||
* @param marketCode Account 关联 Market 代码
|
||||
* @param marketName Account 关联 Market 显示名称
|
||||
* @param sourceCode Account 关联 Source 代码
|
||||
* @param sourceName Account 关联 Source 显示名称
|
||||
* @param adultCapacity 房型成人容量,未配置时为空
|
||||
* @param pricingAvailable Rate Code 是否已有价格能力,第一版固定为 false
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record ReservationV4CatalogLookupItemResult(
|
||||
String code,
|
||||
@JsonProperty("display_name")
|
||||
String displayName,
|
||||
String status,
|
||||
@JsonProperty("catalog_source")
|
||||
String catalogSource,
|
||||
@JsonProperty("market_code")
|
||||
String marketCode,
|
||||
@JsonProperty("market_name")
|
||||
String marketName,
|
||||
@JsonProperty("source_code")
|
||||
String sourceCode,
|
||||
@JsonProperty("source_name")
|
||||
String sourceName,
|
||||
@JsonProperty("adult_capacity")
|
||||
Integer adultCapacity,
|
||||
@JsonProperty("pricing_available")
|
||||
Boolean pricingAvailable
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Reservation V4 lookup 统一 wrapper。前端所有目录下拉都使用同一层级读取。
|
||||
*
|
||||
* @param hotelId 酒店 ID
|
||||
* @param catalogType 目录类型
|
||||
* @param catalogSource 本页目录主要来源
|
||||
* @param catalogVersion 本页目录版本
|
||||
* @param stale 是否为过期或兜底数据
|
||||
* @param items 目录项列表
|
||||
* @param page 分页元数据
|
||||
* @param warnings 安全提示或兜底提示
|
||||
*/
|
||||
public record ReservationV4CatalogLookupResult(
|
||||
@JsonProperty("hotel_id")
|
||||
String hotelId,
|
||||
@JsonProperty("catalog_type")
|
||||
String catalogType,
|
||||
@JsonProperty("catalog_source")
|
||||
String catalogSource,
|
||||
@JsonProperty("catalog_version")
|
||||
String catalogVersion,
|
||||
boolean stale,
|
||||
List<ReservationV4CatalogLookupItemResult> items,
|
||||
ReservationPaginationResult page,
|
||||
List<String> warnings
|
||||
) {
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
@RestControllerAdvice(assignableTypes = {
|
||||
ReservationTaskController.class,
|
||||
ReservationFrontendQueryController.class,
|
||||
ReservationV4CatalogLookupController.class,
|
||||
ReservationV4QueryController.class,
|
||||
ReservationV4CommandController.class,
|
||||
ReservationDemoDataController.class,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode;
|
||||
import cn.nianxx.thhotel.platform.security.service.FrontendAuthorizationService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4CatalogLookupRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4CatalogLookupResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4CatalogLookupService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Reservation V4 目录 lookup Controller。为前端字段控件提供 Account、Room Type、Rate Code 下拉数据。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/reservation/lookups")
|
||||
public class ReservationV4CatalogLookupController {
|
||||
|
||||
private final ReservationV4CatalogLookupService lookupService;
|
||||
private final FrontendAuthorizationService authorizationService;
|
||||
|
||||
/**
|
||||
* 注入目录 lookup 服务和前端鉴权服务,Controller 只负责 HTTP 边界和权限入口。
|
||||
*/
|
||||
public ReservationV4CatalogLookupController(
|
||||
ReservationV4CatalogLookupService lookupService,
|
||||
FrontendAuthorizationService authorizationService) {
|
||||
this.lookupService = lookupService;
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Account 目录,用于 Basic Information 的 Account 选择控件。
|
||||
*/
|
||||
@GetMapping(value = "/accounts", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ReservationV4CatalogLookupResult listAccounts(
|
||||
@RequestParam(name = "hotel_id", required = false) String hotelId,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(name = "page_num", required = false) Integer pageNum,
|
||||
@RequestParam(name = "page_size", required = false) Integer pageSize) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_TASK_READ.name());
|
||||
return lookupService.listAccounts(new ReservationV4CatalogLookupRequest(
|
||||
hotelId,
|
||||
"ACCOUNT",
|
||||
keyword,
|
||||
pageNum,
|
||||
pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Room Type 目录,用于房型选择控件和 V4 字段校验。
|
||||
*/
|
||||
@GetMapping(value = "/room-types", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ReservationV4CatalogLookupResult listRoomTypes(
|
||||
@RequestParam(name = "hotel_id", required = false) String hotelId,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(name = "page_num", required = false) Integer pageNum,
|
||||
@RequestParam(name = "page_size", required = false) Integer pageSize) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_TASK_READ.name());
|
||||
return lookupService.listRoomTypes(new ReservationV4CatalogLookupRequest(
|
||||
hotelId,
|
||||
"ROOM_TYPE",
|
||||
keyword,
|
||||
pageNum,
|
||||
pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Rate Code 目录,用于 Rate Code 选择控件和 V4 字段校验。
|
||||
*/
|
||||
@GetMapping(value = "/rate-codes", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ReservationV4CatalogLookupResult listRateCodes(
|
||||
@RequestParam(name = "hotel_id", required = false) String hotelId,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(name = "page_num", required = false) Integer pageNum,
|
||||
@RequestParam(name = "page_size", required = false) Integer pageSize) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_TASK_READ.name());
|
||||
return lookupService.listRateCodes(new ReservationV4CatalogLookupRequest(
|
||||
hotelId,
|
||||
"RATE_CODE",
|
||||
keyword,
|
||||
pageNum,
|
||||
pageSize));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Reservation Account 目录实体。承载酒店维度的客户/旅行社 Account 代码。
|
||||
*/
|
||||
@TableName("workflow_reservation_catalog_account")
|
||||
public class ReservationCatalogAccountEntity {
|
||||
|
||||
/** 目录记录 ID。 */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
/** 酒店 ID。 */
|
||||
private String hotelId;
|
||||
/** Account 稳定代码。 */
|
||||
private String accountCode;
|
||||
/** Account 显示名称。 */
|
||||
private String accountName;
|
||||
/** Account 关联 Market 代码。 */
|
||||
private String marketCode;
|
||||
/** Account 关联 Source 代码。 */
|
||||
private String sourceCode;
|
||||
/** 目录状态。 */
|
||||
private String status;
|
||||
/** 目录来源。 */
|
||||
private String sourceSystem;
|
||||
/** 外部系统 Account ID。 */
|
||||
private String externalAccountId;
|
||||
/** 目录版本。 */
|
||||
private String catalogVersion;
|
||||
/** 最近同步 UTC 时间。 */
|
||||
private LocalDateTime lastSyncedAt;
|
||||
/** Account 扩展属性 JSON。 */
|
||||
private String metadataJson;
|
||||
/** 乐观锁版本。 */
|
||||
private Long version;
|
||||
/** 记录创建 UTC 时间。 */
|
||||
private LocalDateTime createdAt;
|
||||
/** 记录更新 UTC 时间。 */
|
||||
private LocalDateTime updatedAt;
|
||||
/** 逻辑删除 UTC 时间。 */
|
||||
private LocalDateTime logicDeletedAt;
|
||||
/** 逻辑删除原因。 */
|
||||
private String logicDeletedReason;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getHotelId() { return hotelId; }
|
||||
public void setHotelId(String hotelId) { this.hotelId = hotelId; }
|
||||
public String getAccountCode() { return accountCode; }
|
||||
public void setAccountCode(String accountCode) { this.accountCode = accountCode; }
|
||||
public String getAccountName() { return accountName; }
|
||||
public void setAccountName(String accountName) { this.accountName = accountName; }
|
||||
public String getMarketCode() { return marketCode; }
|
||||
public void setMarketCode(String marketCode) { this.marketCode = marketCode; }
|
||||
public String getSourceCode() { return sourceCode; }
|
||||
public void setSourceCode(String sourceCode) { this.sourceCode = sourceCode; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getSourceSystem() { return sourceSystem; }
|
||||
public void setSourceSystem(String sourceSystem) { this.sourceSystem = sourceSystem; }
|
||||
public String getExternalAccountId() { return externalAccountId; }
|
||||
public void setExternalAccountId(String externalAccountId) { this.externalAccountId = externalAccountId; }
|
||||
public String getCatalogVersion() { return catalogVersion; }
|
||||
public void setCatalogVersion(String catalogVersion) { this.catalogVersion = catalogVersion; }
|
||||
public LocalDateTime getLastSyncedAt() { return lastSyncedAt; }
|
||||
public void setLastSyncedAt(LocalDateTime lastSyncedAt) { this.lastSyncedAt = lastSyncedAt; }
|
||||
public String getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(String metadataJson) { this.metadataJson = metadataJson; }
|
||||
public Long getVersion() { return version; }
|
||||
public void setVersion(Long version) { this.version = version; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public LocalDateTime getLogicDeletedAt() { return logicDeletedAt; }
|
||||
public void setLogicDeletedAt(LocalDateTime logicDeletedAt) { this.logicDeletedAt = logicDeletedAt; }
|
||||
public String getLogicDeletedReason() { return logicDeletedReason; }
|
||||
public void setLogicDeletedReason(String logicDeletedReason) { this.logicDeletedReason = logicDeletedReason; }
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Reservation 通用代码目录实体。承载 Room Type、Rate Code、Market、Source 等酒店目录。
|
||||
*/
|
||||
@TableName("workflow_reservation_catalog_code")
|
||||
public class ReservationCatalogCodeEntity {
|
||||
|
||||
/** 目录记录 ID。 */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
/** 酒店 ID。 */
|
||||
private String hotelId;
|
||||
/** 目录类型。 */
|
||||
private String catalogType;
|
||||
/** 目录稳定代码。 */
|
||||
private String code;
|
||||
/** 目录显示名称。 */
|
||||
private String displayName;
|
||||
/** 目录状态。 */
|
||||
private String status;
|
||||
/** 目录来源。 */
|
||||
private String sourceSystem;
|
||||
/** 外部系统目录 ID。 */
|
||||
private String externalId;
|
||||
/** 展示排序。 */
|
||||
private Integer sortOrder;
|
||||
/** 目录版本。 */
|
||||
private String catalogVersion;
|
||||
/** 最近同步 UTC 时间。 */
|
||||
private LocalDateTime lastSyncedAt;
|
||||
/** 目录扩展属性 JSON。 */
|
||||
private String metadataJson;
|
||||
/** 乐观锁版本。 */
|
||||
private Long version;
|
||||
/** 记录创建 UTC 时间。 */
|
||||
private LocalDateTime createdAt;
|
||||
/** 记录更新 UTC 时间。 */
|
||||
private LocalDateTime updatedAt;
|
||||
/** 逻辑删除 UTC 时间。 */
|
||||
private LocalDateTime logicDeletedAt;
|
||||
/** 逻辑删除原因。 */
|
||||
private String logicDeletedReason;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getHotelId() { return hotelId; }
|
||||
public void setHotelId(String hotelId) { this.hotelId = hotelId; }
|
||||
public String getCatalogType() { return catalogType; }
|
||||
public void setCatalogType(String catalogType) { this.catalogType = catalogType; }
|
||||
public String getCode() { return code; }
|
||||
public void setCode(String code) { this.code = code; }
|
||||
public String getDisplayName() { return displayName; }
|
||||
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getSourceSystem() { return sourceSystem; }
|
||||
public void setSourceSystem(String sourceSystem) { this.sourceSystem = sourceSystem; }
|
||||
public String getExternalId() { return externalId; }
|
||||
public void setExternalId(String externalId) { this.externalId = externalId; }
|
||||
public Integer getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||
public String getCatalogVersion() { return catalogVersion; }
|
||||
public void setCatalogVersion(String catalogVersion) { this.catalogVersion = catalogVersion; }
|
||||
public LocalDateTime getLastSyncedAt() { return lastSyncedAt; }
|
||||
public void setLastSyncedAt(LocalDateTime lastSyncedAt) { this.lastSyncedAt = lastSyncedAt; }
|
||||
public String getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(String metadataJson) { this.metadataJson = metadataJson; }
|
||||
public Long getVersion() { return version; }
|
||||
public void setVersion(Long version) { this.version = version; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public LocalDateTime getLogicDeletedAt() { return logicDeletedAt; }
|
||||
public void setLogicDeletedAt(LocalDateTime logicDeletedAt) { this.logicDeletedAt = logicDeletedAt; }
|
||||
public String getLogicDeletedReason() { return logicDeletedReason; }
|
||||
public void setLogicDeletedReason(String logicDeletedReason) { this.logicDeletedReason = logicDeletedReason; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.mapper;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationCatalogAccountEntity;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* Reservation Account 目录 Mapper。自带 CRUD 由 MyBatis-Plus 提供。
|
||||
*/
|
||||
@Mapper
|
||||
public interface ReservationCatalogAccountMapper extends BaseMapper<ReservationCatalogAccountEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.mapper;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationCatalogCodeEntity;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* Reservation 通用代码目录 Mapper。自带 CRUD 由 MyBatis-Plus 提供。
|
||||
*/
|
||||
@Mapper
|
||||
public interface ReservationCatalogCodeMapper extends BaseMapper<ReservationCatalogCodeEntity> {
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.repository;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationPageSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4CatalogAccountSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4CatalogCodeSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CatalogStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationCatalogAccountEntity;
|
||||
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationCatalogCodeEntity;
|
||||
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationCatalogAccountMapper;
|
||||
import cn.nianxx.thhotel.workflows.reservation.mapper.ReservationCatalogCodeMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Reservation V4 目录 MyBatis-Plus 持久化实现。所有读取默认排除逻辑删除和非 ACTIVE 目录。
|
||||
*/
|
||||
@Repository
|
||||
public class MybatisReservationV4CatalogRepository implements ReservationV4CatalogRepository {
|
||||
|
||||
private final ReservationCatalogAccountMapper accountMapper;
|
||||
private final ReservationCatalogCodeMapper codeMapper;
|
||||
|
||||
/**
|
||||
* 注入目录 Mapper,Repository 统一处理查询条件和快照转换。
|
||||
*/
|
||||
public MybatisReservationV4CatalogRepository(
|
||||
ReservationCatalogAccountMapper accountMapper,
|
||||
ReservationCatalogCodeMapper codeMapper) {
|
||||
this.accountMapper = accountMapper;
|
||||
this.codeMapper = codeMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按酒店和 Account Code 查询 ACTIVE Account 目录。
|
||||
*/
|
||||
@Override
|
||||
public Optional<ReservationV4CatalogAccountSnapshot> findActiveAccount(String hotelId, String accountCode) {
|
||||
String normalizedCode = trimToNull(accountCode);
|
||||
if (trimToNull(hotelId) == null || normalizedCode == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
ReservationCatalogAccountEntity entity = accountMapper.selectOne(
|
||||
Wrappers.<ReservationCatalogAccountEntity>lambdaQuery()
|
||||
.eq(ReservationCatalogAccountEntity::getHotelId, hotelId)
|
||||
.eq(ReservationCatalogAccountEntity::getAccountCode, normalizedCode)
|
||||
.eq(ReservationCatalogAccountEntity::getStatus, ReservationV4CatalogStatus.ACTIVE.name())
|
||||
.isNull(ReservationCatalogAccountEntity::getLogicDeletedAt)
|
||||
.last("LIMIT 1"));
|
||||
return Optional.ofNullable(entity).map(this::toAccountSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按酒店、目录类型和代码查询 ACTIVE 通用目录。
|
||||
*/
|
||||
@Override
|
||||
public Optional<ReservationV4CatalogCodeSnapshot> findActiveCatalogCode(
|
||||
String hotelId,
|
||||
String catalogType,
|
||||
String code) {
|
||||
String normalizedType = trimToNull(catalogType);
|
||||
String normalizedCode = trimToNull(code);
|
||||
if (trimToNull(hotelId) == null || normalizedType == null || normalizedCode == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
ReservationCatalogCodeEntity entity = codeMapper.selectOne(
|
||||
Wrappers.<ReservationCatalogCodeEntity>lambdaQuery()
|
||||
.eq(ReservationCatalogCodeEntity::getHotelId, hotelId)
|
||||
.eq(ReservationCatalogCodeEntity::getCatalogType, normalizedType)
|
||||
.eq(ReservationCatalogCodeEntity::getCode, normalizedCode)
|
||||
.eq(ReservationCatalogCodeEntity::getStatus, ReservationV4CatalogStatus.ACTIVE.name())
|
||||
.isNull(ReservationCatalogCodeEntity::getLogicDeletedAt)
|
||||
.last("LIMIT 1"));
|
||||
return Optional.ofNullable(entity).map(this::toCodeSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询 Account 目录,用于前端 lookup 下拉。
|
||||
*/
|
||||
@Override
|
||||
public ReservationPageSnapshot<ReservationV4CatalogAccountSnapshot> queryActiveAccounts(
|
||||
String hotelId,
|
||||
String keyword,
|
||||
int pageNum,
|
||||
int pageSize) {
|
||||
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))
|
||||
.or()
|
||||
.like(ReservationCatalogAccountEntity::getAccountName, trimToNull(keyword)))
|
||||
.orderByAsc(ReservationCatalogAccountEntity::getAccountCode)
|
||||
.orderByAsc(ReservationCatalogAccountEntity::getId);
|
||||
Page<ReservationCatalogAccountEntity> page = accountMapper.selectPage(Page.of(pageNum, pageSize), query);
|
||||
List<ReservationV4CatalogAccountSnapshot> items = page.getRecords().stream()
|
||||
.map(this::toAccountSnapshot)
|
||||
.toList();
|
||||
return new ReservationPageSnapshot<>(items, page.getTotal(), pageNum, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询通用代码目录,用于前端 Room Type、Rate Code lookup 下拉。
|
||||
*/
|
||||
@Override
|
||||
public ReservationPageSnapshot<ReservationV4CatalogCodeSnapshot> queryActiveCatalogCodes(
|
||||
String hotelId,
|
||||
String catalogType,
|
||||
String keyword,
|
||||
int pageNum,
|
||||
int pageSize) {
|
||||
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))
|
||||
.or()
|
||||
.like(ReservationCatalogCodeEntity::getDisplayName, trimToNull(keyword)))
|
||||
.orderByAsc(ReservationCatalogCodeEntity::getSortOrder)
|
||||
.orderByAsc(ReservationCatalogCodeEntity::getCode)
|
||||
.orderByAsc(ReservationCatalogCodeEntity::getId);
|
||||
Page<ReservationCatalogCodeEntity> page = codeMapper.selectPage(Page.of(pageNum, pageSize), query);
|
||||
List<ReservationV4CatalogCodeSnapshot> items = page.getRecords().stream()
|
||||
.map(this::toCodeSnapshot)
|
||||
.toList();
|
||||
return new ReservationPageSnapshot<>(items, page.getTotal(), pageNum, pageSize);
|
||||
}
|
||||
|
||||
private ReservationV4CatalogAccountSnapshot toAccountSnapshot(ReservationCatalogAccountEntity entity) {
|
||||
return new ReservationV4CatalogAccountSnapshot(
|
||||
entity.getId(),
|
||||
entity.getHotelId(),
|
||||
entity.getAccountCode(),
|
||||
entity.getAccountName(),
|
||||
entity.getMarketCode(),
|
||||
entity.getSourceCode(),
|
||||
entity.getStatus(),
|
||||
entity.getSourceSystem(),
|
||||
entity.getCatalogVersion(),
|
||||
entity.getLastSyncedAt(),
|
||||
entity.getMetadataJson());
|
||||
}
|
||||
|
||||
private ReservationV4CatalogCodeSnapshot toCodeSnapshot(ReservationCatalogCodeEntity entity) {
|
||||
return new ReservationV4CatalogCodeSnapshot(
|
||||
entity.getId(),
|
||||
entity.getHotelId(),
|
||||
entity.getCatalogType(),
|
||||
entity.getCode(),
|
||||
entity.getDisplayName(),
|
||||
entity.getStatus(),
|
||||
entity.getSourceSystem(),
|
||||
entity.getSortOrder(),
|
||||
entity.getCatalogVersion(),
|
||||
entity.getLastSyncedAt(),
|
||||
entity.getMetadataJson());
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.repository;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationPageSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4CatalogAccountSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4CatalogCodeSnapshot;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Reservation V4 目录持久化边界。封装 Account、Room Type、Rate Code 等数据库目录读取。
|
||||
*/
|
||||
public interface ReservationV4CatalogRepository {
|
||||
|
||||
/**
|
||||
* 按酒店和 Account Code 查询 ACTIVE Account 目录。
|
||||
*/
|
||||
Optional<ReservationV4CatalogAccountSnapshot> findActiveAccount(String hotelId, String accountCode);
|
||||
|
||||
/**
|
||||
* 按酒店、目录类型和代码查询 ACTIVE 通用目录。
|
||||
*/
|
||||
Optional<ReservationV4CatalogCodeSnapshot> findActiveCatalogCode(String hotelId, String catalogType, String code);
|
||||
|
||||
/**
|
||||
* 分页查询 Account 目录,用于前端 lookup 下拉。
|
||||
*/
|
||||
ReservationPageSnapshot<ReservationV4CatalogAccountSnapshot> queryActiveAccounts(
|
||||
String hotelId,
|
||||
String keyword,
|
||||
int pageNum,
|
||||
int pageSize);
|
||||
|
||||
/**
|
||||
* 分页查询通用代码目录,用于前端 Room Type、Rate Code lookup 下拉。
|
||||
*/
|
||||
ReservationPageSnapshot<ReservationV4CatalogCodeSnapshot> queryActiveCatalogCodes(
|
||||
String hotelId,
|
||||
String catalogType,
|
||||
String keyword,
|
||||
int pageNum,
|
||||
int pageSize);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4CatalogLookupRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4CatalogLookupResult;
|
||||
|
||||
/**
|
||||
* Reservation V4 目录 lookup 查询服务。面向前端下拉、选择控件提供酒店隔离后的目录数据。
|
||||
*/
|
||||
public interface ReservationV4CatalogLookupService {
|
||||
|
||||
/**
|
||||
* 查询 Account 目录下拉数据。
|
||||
*/
|
||||
ReservationV4CatalogLookupResult listAccounts(ReservationV4CatalogLookupRequest request);
|
||||
|
||||
/**
|
||||
* 查询 Room Type 目录下拉数据。
|
||||
*/
|
||||
ReservationV4CatalogLookupResult listRoomTypes(ReservationV4CatalogLookupRequest request);
|
||||
|
||||
/**
|
||||
* 查询 Rate Code 目录下拉数据。
|
||||
*/
|
||||
ReservationV4CatalogLookupResult listRateCodes(ReservationV4CatalogLookupRequest request);
|
||||
}
|
||||
@@ -4,22 +4,22 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4AccountCa
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Reservation V4 第一版目录服务。封装 Account、房型和 Rate Code 的目录边界。
|
||||
* Reservation V4 目录服务。封装 Account、房型和 Rate Code 的酒店维度目录边界。
|
||||
*/
|
||||
public interface ReservationV4DirectoryService {
|
||||
|
||||
/**
|
||||
* 按 Account Code 查询目录项;不存在时返回空,用于触发人工复核或字段校验错误。
|
||||
* 按酒店和 Account Code 查询目录项;不存在时返回空,用于触发人工复核或字段校验错误。
|
||||
*/
|
||||
Optional<ReservationV4AccountCatalogItem> findAccount(String accountCode);
|
||||
Optional<ReservationV4AccountCatalogItem> findAccount(String hotelId, String accountCode);
|
||||
|
||||
/**
|
||||
* 判断房型代码是否在第一版固定目录中;空值由业务字段必填规则处理。
|
||||
* 判断房型代码是否在当前酒店目录中;空值由业务字段必填规则处理。
|
||||
*/
|
||||
boolean isKnownRoomTypeCode(String roomTypeCode);
|
||||
boolean isKnownRoomTypeCode(String hotelId, String roomTypeCode);
|
||||
|
||||
/**
|
||||
* 判断 Rate Code 是否在第一版固定目录中;空值由业务字段必填规则处理。
|
||||
* 判断 Rate Code 是否在当前酒店目录中;空值由业务字段必填规则处理。
|
||||
*/
|
||||
boolean isKnownRateCode(String rateCode);
|
||||
boolean isKnownRateCode(String hotelId, String rateCode);
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4AccountCatalogItem;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4DirectoryService;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Reservation V4 固定种子目录实现。开发阶段先用代码种子,后续可替换为数据库目录。
|
||||
*/
|
||||
@Service
|
||||
public class FixedReservationV4DirectoryServiceImpl implements ReservationV4DirectoryService {
|
||||
|
||||
private static final Map<String, ReservationV4AccountCatalogItem> ACCOUNT_CATALOG = Map.of(
|
||||
"QBD_TRAVEL", new ReservationV4AccountCatalogItem(
|
||||
"QBD_TRAVEL",
|
||||
"Q.B.D. TRAVEL GROUP CO., LTD",
|
||||
"LEISURE",
|
||||
"TRAVEL_AGENT"),
|
||||
"LIAN_TAI", new ReservationV4AccountCatalogItem(
|
||||
"LIAN_TAI",
|
||||
"LIAN TAI TRAVEL (THAILAND) CO., LTD.",
|
||||
"LEISURE",
|
||||
"TRAVEL_AGENT"),
|
||||
"HANATOUR_TD", new ReservationV4AccountCatalogItem(
|
||||
"HANATOUR_TD",
|
||||
"HANATOUR TD CO., LTD.",
|
||||
"LEISURE",
|
||||
"TRAVEL_AGENT")
|
||||
);
|
||||
private static final Set<String> ROOM_TYPE_CODES = Set.of("TWN", "KING", "DBL", "SGL", "TRP", "RM1", "RM2", "RM3");
|
||||
private static final Set<String> RATE_CODES = Set.of("BAR", "RACK", "PACKAGE", "GROUP", "FIT");
|
||||
|
||||
/**
|
||||
* 按 Account Code 查询固定种子目录,比较保持大小写敏感。
|
||||
*/
|
||||
@Override
|
||||
public Optional<ReservationV4AccountCatalogItem> findAccount(String accountCode) {
|
||||
String normalized = trimToNull(accountCode);
|
||||
return normalized == null ? Optional.empty() : Optional.ofNullable(ACCOUNT_CATALOG.get(normalized));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断房型代码是否存在于第一版固定目录。
|
||||
*/
|
||||
@Override
|
||||
public boolean isKnownRoomTypeCode(String roomTypeCode) {
|
||||
String normalized = trimToNull(roomTypeCode);
|
||||
return normalized != null && ROOM_TYPE_CODES.contains(normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 Rate Code 是否存在于第一版固定目录。
|
||||
*/
|
||||
@Override
|
||||
public boolean isKnownRateCode(String rateCode) {
|
||||
String normalized = trimToNull(rateCode);
|
||||
return normalized != null && RATE_CODES.contains(normalized);
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationPageSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4CatalogAccountSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4CatalogCodeSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CatalogType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4CatalogLookupRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationPaginationResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4CatalogLookupItemResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4CatalogLookupResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4CatalogRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4CatalogLookupService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Reservation V4 目录 lookup 查询服务实现。查询数据库目录并转换为前端统一 wrapper。
|
||||
*/
|
||||
@Service
|
||||
public class ReservationV4CatalogLookupServiceImpl implements ReservationV4CatalogLookupService {
|
||||
|
||||
private static final int DEFAULT_PAGE_NUM = 1;
|
||||
private static final int DEFAULT_PAGE_SIZE = 20;
|
||||
private static final int MAX_PAGE_NUM = 1000;
|
||||
private static final int MAX_PAGE_SIZE = 100;
|
||||
private static final String EMPTY_CATALOG_SOURCE = "DATABASE_EMPTY";
|
||||
private static final String EMPTY_CATALOG_VERSION = "unknown";
|
||||
|
||||
private final ReservationV4CatalogRepository catalogRepository;
|
||||
private final HotelContextService hotelContextService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 注入目录 Repository、酒店上下文和 JSON 解析器,Service 负责权限后的酒店隔离和响应组装。
|
||||
*/
|
||||
public ReservationV4CatalogLookupServiceImpl(
|
||||
ReservationV4CatalogRepository catalogRepository,
|
||||
HotelContextService hotelContextService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.catalogRepository = catalogRepository;
|
||||
this.hotelContextService = hotelContextService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Account 目录下拉数据。
|
||||
*/
|
||||
@Override
|
||||
public ReservationV4CatalogLookupResult listAccounts(ReservationV4CatalogLookupRequest request) {
|
||||
String hotelId = resolveHotelId(request == null ? null : request.hotelId());
|
||||
int pageNum = normalizePageNum(request == null ? null : request.pageNum());
|
||||
int pageSize = normalizePageSize(request == null ? null : request.pageSize());
|
||||
ReservationPageSnapshot<ReservationV4CatalogAccountSnapshot> page = catalogRepository.queryActiveAccounts(
|
||||
hotelId,
|
||||
request == null ? null : request.keyword(),
|
||||
pageNum,
|
||||
pageSize);
|
||||
List<ReservationV4CatalogLookupItemResult> items = page.items().stream()
|
||||
.map(this::accountItem)
|
||||
.toList();
|
||||
return new ReservationV4CatalogLookupResult(
|
||||
hotelId,
|
||||
ReservationV4CatalogType.ACCOUNT.name(),
|
||||
sourceSystem(page.items()),
|
||||
catalogVersion(page.items()),
|
||||
false,
|
||||
items,
|
||||
new ReservationPaginationResult(page.pageNum(), page.pageSize(), page.total()),
|
||||
warnings(page.items()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Room Type 目录下拉数据。
|
||||
*/
|
||||
@Override
|
||||
public ReservationV4CatalogLookupResult listRoomTypes(ReservationV4CatalogLookupRequest request) {
|
||||
return listCatalogCodes(request, ReservationV4CatalogType.ROOM_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Rate Code 目录下拉数据。
|
||||
*/
|
||||
@Override
|
||||
public ReservationV4CatalogLookupResult listRateCodes(ReservationV4CatalogLookupRequest request) {
|
||||
return listCatalogCodes(request, ReservationV4CatalogType.RATE_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Room Type / Rate Code 等通用代码目录,并包装为统一 lookup 响应。
|
||||
*/
|
||||
private ReservationV4CatalogLookupResult listCatalogCodes(
|
||||
ReservationV4CatalogLookupRequest request,
|
||||
ReservationV4CatalogType catalogType) {
|
||||
String hotelId = resolveHotelId(request == null ? null : request.hotelId());
|
||||
int pageNum = normalizePageNum(request == null ? null : request.pageNum());
|
||||
int pageSize = normalizePageSize(request == null ? null : request.pageSize());
|
||||
ReservationPageSnapshot<ReservationV4CatalogCodeSnapshot> page = catalogRepository.queryActiveCatalogCodes(
|
||||
hotelId,
|
||||
catalogType.name(),
|
||||
request == null ? null : request.keyword(),
|
||||
pageNum,
|
||||
pageSize);
|
||||
List<ReservationV4CatalogLookupItemResult> items = page.items().stream()
|
||||
.map(snapshot -> codeItem(catalogType, snapshot))
|
||||
.toList();
|
||||
return new ReservationV4CatalogLookupResult(
|
||||
hotelId,
|
||||
catalogType.name(),
|
||||
sourceSystem(page.items()),
|
||||
catalogVersion(page.items()),
|
||||
false,
|
||||
items,
|
||||
new ReservationPaginationResult(page.pageNum(), page.pageSize(), page.total()),
|
||||
warnings(page.items()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Account 快照转换为前端统一目录项,并补齐 Market / Source 显示名。
|
||||
*/
|
||||
private ReservationV4CatalogLookupItemResult accountItem(ReservationV4CatalogAccountSnapshot snapshot) {
|
||||
String marketName = catalogRepository.findActiveCatalogCode(
|
||||
snapshot.hotelId(),
|
||||
ReservationV4CatalogType.MARKET.name(),
|
||||
snapshot.marketCode())
|
||||
.map(ReservationV4CatalogCodeSnapshot::displayName)
|
||||
.orElse(snapshot.marketCode());
|
||||
String sourceName = catalogRepository.findActiveCatalogCode(
|
||||
snapshot.hotelId(),
|
||||
ReservationV4CatalogType.SOURCE.name(),
|
||||
snapshot.sourceCode())
|
||||
.map(ReservationV4CatalogCodeSnapshot::displayName)
|
||||
.orElse(snapshot.sourceCode());
|
||||
return new ReservationV4CatalogLookupItemResult(
|
||||
snapshot.accountCode(),
|
||||
snapshot.accountName(),
|
||||
snapshot.status(),
|
||||
snapshot.sourceSystem(),
|
||||
snapshot.marketCode(),
|
||||
marketName,
|
||||
snapshot.sourceCode(),
|
||||
sourceName,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将通用代码快照转换为前端统一目录项,并按目录类型解析扩展 metadata。
|
||||
*/
|
||||
private ReservationV4CatalogLookupItemResult codeItem(
|
||||
ReservationV4CatalogType catalogType,
|
||||
ReservationV4CatalogCodeSnapshot snapshot) {
|
||||
return new ReservationV4CatalogLookupItemResult(
|
||||
snapshot.code(),
|
||||
snapshot.displayName(),
|
||||
snapshot.status(),
|
||||
snapshot.sourceSystem(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
catalogType == ReservationV4CatalogType.ROOM_TYPE ? metadataInteger(snapshot.metadataJson(), "adult_capacity") : null,
|
||||
catalogType == ReservationV4CatalogType.RATE_CODE ? metadataBoolean(snapshot.metadataJson(), "pricing_available", false) : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从当前页第一条目录项推导目录来源;空页返回数据库空目录标记。
|
||||
*/
|
||||
private String sourceSystem(List<?> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return EMPTY_CATALOG_SOURCE;
|
||||
}
|
||||
Object first = items.get(0);
|
||||
if (first instanceof ReservationV4CatalogAccountSnapshot account) {
|
||||
return account.sourceSystem();
|
||||
}
|
||||
if (first instanceof ReservationV4CatalogCodeSnapshot code) {
|
||||
return code.sourceSystem();
|
||||
}
|
||||
return EMPTY_CATALOG_SOURCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从当前页第一条目录项推导目录版本;空页返回 unknown,提示前端不可缓存为正式版本。
|
||||
*/
|
||||
private String catalogVersion(List<?> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return EMPTY_CATALOG_VERSION;
|
||||
}
|
||||
Object first = items.get(0);
|
||||
if (first instanceof ReservationV4CatalogAccountSnapshot account) {
|
||||
return account.catalogVersion();
|
||||
}
|
||||
if (first instanceof ReservationV4CatalogCodeSnapshot code) {
|
||||
return code.catalogVersion();
|
||||
}
|
||||
return EMPTY_CATALOG_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据目录来源生成前端可展示的非阻塞提示。
|
||||
*/
|
||||
private List<String> warnings(List<?> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return List.of("当前酒店暂无可用目录数据。");
|
||||
}
|
||||
if ("FIXED_SEED_IMPORT".equals(sourceSystem(items))) {
|
||||
return List.of("当前目录来自固定种子初始化,后续接入真实 PMS 或后台维护后会替换。");
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从目录 metadata JSON 中读取整数扩展字段,格式异常时安全降级为空。
|
||||
*/
|
||||
private Integer metadataInteger(String metadataJson, String fieldName) {
|
||||
JsonNode node = metadataNode(metadataJson);
|
||||
if (node == null || !node.has(fieldName) || !node.get(fieldName).canConvertToInt()) {
|
||||
return null;
|
||||
}
|
||||
return node.get(fieldName).asInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从目录 metadata JSON 中读取布尔扩展字段,缺失或格式异常时返回默认值。
|
||||
*/
|
||||
private Boolean metadataBoolean(String metadataJson, String fieldName, boolean defaultValue) {
|
||||
JsonNode node = metadataNode(metadataJson);
|
||||
if (node == null || !node.has(fieldName)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return node.get(fieldName).asBoolean(defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析目录 metadata JSON,坏数据不向前端透出异常细节。
|
||||
*/
|
||||
private JsonNode metadataNode(String metadataJson) {
|
||||
String normalized = trimToNull(metadataJson);
|
||||
if (normalized == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readTree(normalized);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前请求酒店并套用登录用户的酒店访问权校验。
|
||||
*/
|
||||
private String resolveHotelId(String hotelId) {
|
||||
try {
|
||||
return hotelContextService.resolveCurrentHotelId(hotelId);
|
||||
} catch (HotelContextException exception) {
|
||||
throw new ReservationTaskWorkflowException(
|
||||
exception.getStatus(),
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化页码,避免异常大页码或非法页码影响查询稳定性。
|
||||
*/
|
||||
private int normalizePageNum(Integer pageNum) {
|
||||
if (pageNum == null || pageNum < 1) {
|
||||
return DEFAULT_PAGE_NUM;
|
||||
}
|
||||
return Math.min(pageNum, MAX_PAGE_NUM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化分页大小,第一版限制单次最多返回 100 条。
|
||||
*/
|
||||
private int normalizePageSize(Integer pageSize) {
|
||||
if (pageSize == null || pageSize < 1) {
|
||||
return DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
return Math.min(pageSize, MAX_PAGE_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除字符串首尾空白,空白值统一视为 null。
|
||||
*/
|
||||
private String trimToNull(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,10 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
ensureNoPriorOrderTaskBlocking(orderTask);
|
||||
ensureBasicInformationConfirmed(orderTask, card);
|
||||
|
||||
String confirmedPayloadJson = confirmedPayloadJson(card, request == null ? null : request.confirmedPayload());
|
||||
String confirmedPayloadJson = confirmedPayloadJson(
|
||||
orderTask.hotelId(),
|
||||
card,
|
||||
request == null ? null : request.confirmedPayload());
|
||||
String actorId = actorIdentifier(actor);
|
||||
boolean updated = workflowRepository.confirmTaskCardWithVersion(
|
||||
orderTask.hotelId(),
|
||||
@@ -186,7 +189,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
card,
|
||||
confirmedPayload,
|
||||
request == null ? null : request.fieldOverrides());
|
||||
validateAndEnrichConfirmedPayload(card, confirmedPayload);
|
||||
validateAndEnrichConfirmedPayload(orderTask.hotelId(), card, confirmedPayload);
|
||||
String actorId = actorIdentifier(actor);
|
||||
String confirmedPayloadJson = toJson(confirmedPayload);
|
||||
String reviewResolutionJson = reviewResolutionJson(
|
||||
@@ -469,9 +472,9 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
throw error(HttpStatus.CONFLICT, "V4_CARD_VERSION_CONFLICT", "任务卡版本已变化,请刷新后重试。");
|
||||
}
|
||||
|
||||
private String confirmedPayloadJson(ReservationV4TaskCardSnapshot card, JsonNode confirmedPayload) {
|
||||
private String confirmedPayloadJson(String hotelId, ReservationV4TaskCardSnapshot card, JsonNode confirmedPayload) {
|
||||
ObjectNode payload = confirmedPayloadObject(card, confirmedPayload);
|
||||
validateAndEnrichConfirmedPayload(card, payload);
|
||||
validateAndEnrichConfirmedPayload(hotelId, card, payload);
|
||||
return toJson(payload);
|
||||
}
|
||||
|
||||
@@ -610,12 +613,12 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
return wrappedBusinessFields || BUSINESS_WRITABLE_ROOT_FIELDS.contains(path.get(0));
|
||||
}
|
||||
|
||||
private void validateAndEnrichConfirmedPayload(ReservationV4TaskCardSnapshot card, ObjectNode payload) {
|
||||
private void validateAndEnrichConfirmedPayload(String hotelId, ReservationV4TaskCardSnapshot card, ObjectNode payload) {
|
||||
List<String> details = new ArrayList<>();
|
||||
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
|
||||
validateAndEnrichBasicInformation(payload, details);
|
||||
validateAndEnrichBasicInformation(hotelId, payload, details);
|
||||
} else {
|
||||
validateBusinessCardPayload(payload, details);
|
||||
validateBusinessCardPayload(hotelId, payload, details);
|
||||
}
|
||||
if (!details.isEmpty()) {
|
||||
throw new ReservationTaskWorkflowException(
|
||||
@@ -626,14 +629,14 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAndEnrichBasicInformation(ObjectNode payload, List<String> details) {
|
||||
private void validateAndEnrichBasicInformation(String hotelId, ObjectNode payload, List<String> details) {
|
||||
ObjectNode basicInformation = ensureBasicInformationObject(payload);
|
||||
String accountCode = textAt(basicInformation, "account_code");
|
||||
if (accountCode == null) {
|
||||
details.add("basic_information.account_code: Account Code 不能为空。");
|
||||
return;
|
||||
}
|
||||
ReservationV4AccountCatalogItem account = directoryService.findAccount(accountCode).orElse(null);
|
||||
ReservationV4AccountCatalogItem account = directoryService.findAccount(hotelId, accountCode).orElse(null);
|
||||
if (account == null) {
|
||||
details.add("basic_information.account_code: Account Code 不在信息系统目录中。");
|
||||
return;
|
||||
@@ -663,13 +666,13 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
}
|
||||
}
|
||||
|
||||
private void validateBusinessCardPayload(ObjectNode payload, List<String> details) {
|
||||
private void validateBusinessCardPayload(String hotelId, ObjectNode payload, List<String> details) {
|
||||
JsonNode businessFields = payload.path("business_fields");
|
||||
JsonNode fieldRoot = businessFields.isObject() ? businessFields : payload;
|
||||
validateBusinessCatalogFields(fieldRoot, "business_fields", details);
|
||||
validateBusinessCatalogFields(hotelId, fieldRoot, "business_fields", details);
|
||||
}
|
||||
|
||||
private void validateBusinessCatalogFields(JsonNode node, String fieldPath, List<String> details) {
|
||||
private void validateBusinessCatalogFields(String hotelId, JsonNode node, String fieldPath, List<String> details) {
|
||||
if (node == null || node.isMissingNode() || node.isNull()) {
|
||||
return;
|
||||
}
|
||||
@@ -679,30 +682,39 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
Map.Entry<String, JsonNode> field = fields.next();
|
||||
String childPath = fieldPath + "." + field.getKey();
|
||||
if ("rate_code".equals(field.getKey())) {
|
||||
validateRateCode(field.getValue(), childPath, details);
|
||||
validateRateCode(hotelId, field.getValue(), childPath, details);
|
||||
} else if ("target_room_type_code".equals(field.getKey())) {
|
||||
validateRoomTypeCode(hotelId, field.getValue(), childPath, details);
|
||||
} else if ("room_items".equals(field.getKey())) {
|
||||
validateRoomItems(field.getValue(), childPath, details);
|
||||
validateRoomItems(hotelId, field.getValue(), childPath, details);
|
||||
} else {
|
||||
validateBusinessCatalogFields(field.getValue(), childPath, details);
|
||||
validateBusinessCatalogFields(hotelId, field.getValue(), childPath, details);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.isArray()) {
|
||||
for (int index = 0; index < node.size(); index++) {
|
||||
validateBusinessCatalogFields(node.get(index), fieldPath + "." + index, details);
|
||||
validateBusinessCatalogFields(hotelId, node.get(index), fieldPath + "." + index, details);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRateCode(JsonNode rateCode, String fieldPath, List<String> details) {
|
||||
private void validateRateCode(String hotelId, JsonNode rateCode, String fieldPath, List<String> details) {
|
||||
String code = textValue(rateCode);
|
||||
if (code != null && !directoryService.isKnownRateCode(code)) {
|
||||
if (code != null && !directoryService.isKnownRateCode(hotelId, code)) {
|
||||
details.add(fieldPath + ": Rate Code 不在第一版目录中。");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRoomItems(JsonNode roomItems, String fieldPath, List<String> details) {
|
||||
private void validateRoomTypeCode(String hotelId, JsonNode roomTypeCode, String fieldPath, List<String> details) {
|
||||
String code = textValue(roomTypeCode);
|
||||
if (code != null && !directoryService.isKnownRoomTypeCode(hotelId, code)) {
|
||||
details.add(fieldPath + ": 房型代码不在第一版目录中。");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRoomItems(String hotelId, JsonNode roomItems, String fieldPath, List<String> details) {
|
||||
if (roomItems == null || roomItems.isMissingNode() || roomItems.isNull()) {
|
||||
return;
|
||||
}
|
||||
@@ -716,10 +728,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
details.add(fieldPath + "." + index + ": 房型明细必须是对象。");
|
||||
continue;
|
||||
}
|
||||
String roomTypeCode = textAt(item, "room_type_code");
|
||||
if (roomTypeCode != null && !directoryService.isKnownRoomTypeCode(roomTypeCode)) {
|
||||
details.add(fieldPath + "." + index + ".room_type_code: 房型代码不在第一版目录中。");
|
||||
}
|
||||
validateRoomTypeCode(hotelId, item.get("room_type_code"), fieldPath + "." + index + ".room_type_code", details);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4AccountCatalogItem;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CatalogType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4CatalogRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4DirectoryService;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Reservation V4 数据库目录服务实现。优先使用酒店维度目录表,不再把固定种子作为全局运行时事实。
|
||||
*/
|
||||
@Service
|
||||
public class ReservationV4DatabaseDirectoryServiceImpl implements ReservationV4DirectoryService {
|
||||
|
||||
private final ReservationV4CatalogRepository catalogRepository;
|
||||
|
||||
/**
|
||||
* 注入目录 Repository,目录服务负责把持久化快照转换为业务校验需要的稳定目录项。
|
||||
*/
|
||||
public ReservationV4DatabaseDirectoryServiceImpl(ReservationV4CatalogRepository catalogRepository) {
|
||||
this.catalogRepository = catalogRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按酒店和 Account Code 查询目录项;不存在时返回空,用于触发人工复核或字段校验错误。
|
||||
*/
|
||||
@Override
|
||||
public Optional<ReservationV4AccountCatalogItem> findAccount(String hotelId, String accountCode) {
|
||||
return catalogRepository.findActiveAccount(hotelId, accountCode)
|
||||
.map(snapshot -> new ReservationV4AccountCatalogItem(
|
||||
snapshot.accountCode(),
|
||||
snapshot.accountName(),
|
||||
snapshot.marketCode(),
|
||||
snapshot.sourceCode()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断房型代码是否在当前酒店 ACTIVE Room Type 目录中。
|
||||
*/
|
||||
@Override
|
||||
public boolean isKnownRoomTypeCode(String hotelId, String roomTypeCode) {
|
||||
return catalogRepository.findActiveCatalogCode(
|
||||
hotelId,
|
||||
ReservationV4CatalogType.ROOM_TYPE.name(),
|
||||
roomTypeCode).isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 Rate Code 是否在当前酒店 ACTIVE Rate Code 目录中。
|
||||
*/
|
||||
@Override
|
||||
public boolean isKnownRateCode(String hotelId, String rateCode) {
|
||||
return catalogRepository.findActiveCatalogCode(
|
||||
hotelId,
|
||||
ReservationV4CatalogType.RATE_CODE.name(),
|
||||
rateCode).isPresent();
|
||||
}
|
||||
}
|
||||
@@ -802,7 +802,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
return List.of();
|
||||
}
|
||||
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
|
||||
return basicInformationFields(displayPayload, validationErrors, availability);
|
||||
return basicInformationFields(card.hotelId(), displayPayload, validationErrors, availability);
|
||||
}
|
||||
if (ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name().equals(card.cardType())
|
||||
|| ReservationV4CardType.SOURCE_MESSAGE_NOTIFICATION.name().equals(card.cardType())) {
|
||||
@@ -812,12 +812,13 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
}
|
||||
|
||||
private List<ReservationV4TaskCardFieldResult> basicInformationFields(
|
||||
String hotelId,
|
||||
JsonNode displayPayload,
|
||||
JsonNode validationErrors,
|
||||
ReservationV4ActionAvailabilityResult availability) {
|
||||
JsonNode basicInformation = basicInformationNode(displayPayload);
|
||||
String accountCode = textAt(basicInformation, "account_code");
|
||||
Optional<ReservationV4AccountCatalogItem> account = directoryService.findAccount(accountCode);
|
||||
Optional<ReservationV4AccountCatalogItem> account = directoryService.findAccount(hotelId, accountCode);
|
||||
String marketCode = firstText(textAt(basicInformation, "market_code"), account.map(ReservationV4AccountCatalogItem::marketCode).orElse(null));
|
||||
String sourceCode = firstText(textAt(basicInformation, "source_code"), account.map(ReservationV4AccountCatalogItem::sourceCode).orElse(null));
|
||||
List<ReservationV4TaskCardFieldResult> fields = new ArrayList<>();
|
||||
@@ -834,7 +835,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
"reservation_v4_account_catalog",
|
||||
false,
|
||||
validationMessages(validationErrors, "/basic_information/account_code", "basic_information.account_code"),
|
||||
"第一版 Account 使用后端固定目录,不能自由输入。"));
|
||||
"Account 使用酒店目录 lookup,不能自由输入。"));
|
||||
fields.add(field(
|
||||
"basic_information.market_code",
|
||||
"/basic_information/market_code",
|
||||
|
||||
@@ -182,7 +182,7 @@ public class ReservationV4TaskIntakeServiceImpl implements ReservationV4TaskInta
|
||||
ObjectNode safeBasicInformation = basicInformation.isObject()
|
||||
? ((ObjectNode) basicInformation).deepCopy()
|
||||
: objectMapper.createObjectNode();
|
||||
List<String> validationDetails = validateAndEnrichBasicInformation(safeBasicInformation);
|
||||
List<String> validationDetails = validateAndEnrichBasicInformation(sourceMessage.hotelId(), safeBasicInformation);
|
||||
boolean reviewRequired = manualReviewRequired || !validationDetails.isEmpty();
|
||||
ObjectNode displayPayload = objectMapper.createObjectNode();
|
||||
displayPayload.put("card_type", ReservationV4CardType.BASIC_INFORMATION.name());
|
||||
@@ -215,7 +215,7 @@ public class ReservationV4TaskIntakeServiceImpl implements ReservationV4TaskInta
|
||||
ReservationV4AcceptedEventDraft acceptedEvent,
|
||||
LocalDateTime now) {
|
||||
ObjectNode displayPayload = objectJson(acceptedEvent.displayPayloadJson());
|
||||
List<String> validationDetails = validateBusinessCardDisplayPayload(displayPayload);
|
||||
List<String> validationDetails = validateBusinessCardDisplayPayload(sourceMessage.hotelId(), displayPayload);
|
||||
boolean reviewRequired = acceptedEvent.manualReviewRequired() || !validationDetails.isEmpty();
|
||||
workflowRepository.insertTaskCard(new ReservationV4TaskCardDraft(
|
||||
sourceMessage.hotelId(),
|
||||
@@ -376,14 +376,14 @@ public class ReservationV4TaskIntakeServiceImpl implements ReservationV4TaskInta
|
||||
/**
|
||||
* 校验并派生 Basic Information 的 Account 目录字段,错误只写展示卡,不改 AI 原始 payload。
|
||||
*/
|
||||
private List<String> validateAndEnrichBasicInformation(ObjectNode basicInformation) {
|
||||
private List<String> validateAndEnrichBasicInformation(String hotelId, ObjectNode basicInformation) {
|
||||
List<String> details = new ArrayList<>();
|
||||
String accountCode = trimToNull(textAt(basicInformation, "account_code"));
|
||||
if (accountCode == null) {
|
||||
details.add("basic_information.account_code: Account Code 不能为空。");
|
||||
return details;
|
||||
}
|
||||
ReservationV4AccountCatalogItem account = directoryService.findAccount(accountCode).orElse(null);
|
||||
ReservationV4AccountCatalogItem account = directoryService.findAccount(hotelId, accountCode).orElse(null);
|
||||
if (account == null) {
|
||||
details.add("basic_information.account_code: Account Code 不在信息系统目录中。");
|
||||
return details;
|
||||
@@ -397,15 +397,15 @@ public class ReservationV4TaskIntakeServiceImpl implements ReservationV4TaskInta
|
||||
/**
|
||||
* 校验 V4 业务卡展示字段中的第一版目录值,失败时卡片进入人工复核。
|
||||
*/
|
||||
private List<String> validateBusinessCardDisplayPayload(ObjectNode displayPayload) {
|
||||
private List<String> validateBusinessCardDisplayPayload(String hotelId, ObjectNode displayPayload) {
|
||||
List<String> details = new ArrayList<>();
|
||||
JsonNode businessFields = displayPayload.path("business_fields");
|
||||
JsonNode fieldRoot = businessFields.isObject() ? businessFields : displayPayload;
|
||||
validateBusinessCatalogFields(fieldRoot, "business_fields", details);
|
||||
validateBusinessCatalogFields(hotelId, fieldRoot, "business_fields", details);
|
||||
return details;
|
||||
}
|
||||
|
||||
private void validateBusinessCatalogFields(JsonNode node, String fieldPath, List<String> details) {
|
||||
private void validateBusinessCatalogFields(String hotelId, JsonNode node, String fieldPath, List<String> details) {
|
||||
if (node == null || node.isMissingNode() || node.isNull()) {
|
||||
return;
|
||||
}
|
||||
@@ -415,30 +415,39 @@ public class ReservationV4TaskIntakeServiceImpl implements ReservationV4TaskInta
|
||||
Map.Entry<String, JsonNode> field = fields.next();
|
||||
String childPath = fieldPath + "." + field.getKey();
|
||||
if ("rate_code".equals(field.getKey())) {
|
||||
validateRateCode(field.getValue(), childPath, details);
|
||||
validateRateCode(hotelId, field.getValue(), childPath, details);
|
||||
} else if ("target_room_type_code".equals(field.getKey())) {
|
||||
validateRoomTypeCode(hotelId, field.getValue(), childPath, details);
|
||||
} else if ("room_items".equals(field.getKey())) {
|
||||
validateRoomItems(field.getValue(), childPath, details);
|
||||
validateRoomItems(hotelId, field.getValue(), childPath, details);
|
||||
} else {
|
||||
validateBusinessCatalogFields(field.getValue(), childPath, details);
|
||||
validateBusinessCatalogFields(hotelId, field.getValue(), childPath, details);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.isArray()) {
|
||||
for (int index = 0; index < node.size(); index++) {
|
||||
validateBusinessCatalogFields(node.get(index), fieldPath + "." + index, details);
|
||||
validateBusinessCatalogFields(hotelId, node.get(index), fieldPath + "." + index, details);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRateCode(JsonNode rateCode, String fieldPath, List<String> details) {
|
||||
private void validateRateCode(String hotelId, JsonNode rateCode, String fieldPath, List<String> details) {
|
||||
String code = trimToNull(rateCode == null || !rateCode.isTextual() ? null : rateCode.asText());
|
||||
if (code != null && !directoryService.isKnownRateCode(code)) {
|
||||
if (code != null && !directoryService.isKnownRateCode(hotelId, code)) {
|
||||
details.add(fieldPath + ": Rate Code 不在第一版目录中。");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRoomItems(JsonNode roomItems, String fieldPath, List<String> details) {
|
||||
private void validateRoomTypeCode(String hotelId, JsonNode roomTypeCode, String fieldPath, List<String> details) {
|
||||
String code = trimToNull(roomTypeCode == null || !roomTypeCode.isTextual() ? null : roomTypeCode.asText());
|
||||
if (code != null && !directoryService.isKnownRoomTypeCode(hotelId, code)) {
|
||||
details.add(fieldPath + ": 房型代码不在第一版目录中。");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRoomItems(String hotelId, JsonNode roomItems, String fieldPath, List<String> details) {
|
||||
if (roomItems == null || roomItems.isMissingNode() || roomItems.isNull()) {
|
||||
return;
|
||||
}
|
||||
@@ -452,10 +461,7 @@ public class ReservationV4TaskIntakeServiceImpl implements ReservationV4TaskInta
|
||||
details.add(fieldPath + "." + index + ": 房型明细必须是对象。");
|
||||
continue;
|
||||
}
|
||||
String roomTypeCode = trimToNull(textAt(item, "room_type_code"));
|
||||
if (roomTypeCode != null && !directoryService.isKnownRoomTypeCode(roomTypeCode)) {
|
||||
details.add(fieldPath + "." + index + ".room_type_code: 房型代码不在第一版目录中。");
|
||||
}
|
||||
validateRoomTypeCode(hotelId, item.get("room_type_code"), fieldPath + "." + index + ".room_type_code", details);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user