修复V4目录初始化和lookup空结果元数据
This commit is contained in:
@@ -25,6 +25,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -32,6 +34,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* M003 登录权限底座启动初始化。只写入系统内置数据和首个超级管理员。
|
||||
*/
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public class PlatformIdentityBootstrapRunner implements ApplicationRunner {
|
||||
|
||||
private final AuthProperties authProperties;
|
||||
|
||||
@@ -132,6 +132,70 @@ public class MybatisReservationV4CatalogRepository implements ReservationV4Catal
|
||||
return new ReservationPageSnapshot<>(items, page.getTotal(), pageNum, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前酒店是否已有 Account 目录记录;包含非 ACTIVE 记录,避免启动种子覆盖人工维护数据。
|
||||
*/
|
||||
@Override
|
||||
public boolean hasAnyAccount(String hotelId) {
|
||||
String normalizedHotelId = trimToNull(hotelId);
|
||||
if (normalizedHotelId == null) {
|
||||
return false;
|
||||
}
|
||||
return accountMapper.selectCount(Wrappers.<ReservationCatalogAccountEntity>lambdaQuery()
|
||||
.eq(ReservationCatalogAccountEntity::getHotelId, normalizedHotelId)) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前酒店是否已有通用代码目录记录;包含非 ACTIVE 记录,避免启动种子覆盖人工维护数据。
|
||||
*/
|
||||
@Override
|
||||
public boolean hasAnyCatalogCode(String hotelId) {
|
||||
String normalizedHotelId = trimToNull(hotelId);
|
||||
if (normalizedHotelId == null) {
|
||||
return false;
|
||||
}
|
||||
return codeMapper.selectCount(Wrappers.<ReservationCatalogCodeEntity>lambdaQuery()
|
||||
.eq(ReservationCatalogCodeEntity::getHotelId, normalizedHotelId)) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account Code 不存在时插入目录种子,存在时跳过以保持启动幂等。
|
||||
*/
|
||||
@Override
|
||||
public void insertAccountIfAbsent(ReservationCatalogAccountEntity entity) {
|
||||
if (entity == null
|
||||
|| trimToNull(entity.getHotelId()) == null
|
||||
|| trimToNull(entity.getAccountCode()) == null) {
|
||||
return;
|
||||
}
|
||||
Long existing = accountMapper.selectCount(Wrappers.<ReservationCatalogAccountEntity>lambdaQuery()
|
||||
.eq(ReservationCatalogAccountEntity::getHotelId, entity.getHotelId())
|
||||
.eq(ReservationCatalogAccountEntity::getAccountCode, entity.getAccountCode()));
|
||||
if (existing == null || existing == 0) {
|
||||
accountMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用目录 Code 不存在时插入目录种子,存在时跳过以保持启动幂等。
|
||||
*/
|
||||
@Override
|
||||
public void insertCatalogCodeIfAbsent(ReservationCatalogCodeEntity entity) {
|
||||
if (entity == null
|
||||
|| trimToNull(entity.getHotelId()) == null
|
||||
|| trimToNull(entity.getCatalogType()) == null
|
||||
|| trimToNull(entity.getCode()) == null) {
|
||||
return;
|
||||
}
|
||||
Long existing = codeMapper.selectCount(Wrappers.<ReservationCatalogCodeEntity>lambdaQuery()
|
||||
.eq(ReservationCatalogCodeEntity::getHotelId, entity.getHotelId())
|
||||
.eq(ReservationCatalogCodeEntity::getCatalogType, entity.getCatalogType())
|
||||
.eq(ReservationCatalogCodeEntity::getCode, entity.getCode()));
|
||||
if (existing == null || existing == 0) {
|
||||
codeMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private ReservationV4CatalogAccountSnapshot toAccountSnapshot(ReservationCatalogAccountEntity entity) {
|
||||
return new ReservationV4CatalogAccountSnapshot(
|
||||
entity.getId(),
|
||||
|
||||
@@ -3,6 +3,8 @@ 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.domain.ReservationCatalogAccountEntity;
|
||||
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationCatalogCodeEntity;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -38,4 +40,24 @@ public interface ReservationV4CatalogRepository {
|
||||
String keyword,
|
||||
int pageNum,
|
||||
int pageSize);
|
||||
|
||||
/**
|
||||
* 判断当前酒店是否已有任意 Account 目录记录,用于启动初始化避免覆盖人工目录。
|
||||
*/
|
||||
boolean hasAnyAccount(String hotelId);
|
||||
|
||||
/**
|
||||
* 判断当前酒店是否已有任意通用代码目录记录,用于启动初始化避免覆盖人工目录。
|
||||
*/
|
||||
boolean hasAnyCatalogCode(String hotelId);
|
||||
|
||||
/**
|
||||
* Account Code 不存在时插入固定种子目录,存在时保持原数据不变。
|
||||
*/
|
||||
void insertAccountIfAbsent(ReservationCatalogAccountEntity entity);
|
||||
|
||||
/**
|
||||
* 通用目录 Code 不存在时插入固定种子目录,存在时保持原数据不变。
|
||||
*/
|
||||
void insertCatalogCodeIfAbsent(ReservationCatalogCodeEntity entity);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.hotel.domain.PlatformHotelEntity;
|
||||
import cn.nianxx.thhotel.platform.hotel.repository.PlatformHotelRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CatalogSourceSystem;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CatalogStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CatalogType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationCatalogAccountEntity;
|
||||
import cn.nianxx.thhotel.workflows.reservation.domain.ReservationCatalogCodeEntity;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4CatalogRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Reservation V4 目录启动初始化器。补齐 Flyway 之后由平台启动流程创建的默认酒店目录种子。
|
||||
*/
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE + 100)
|
||||
public class ReservationV4CatalogBootstrapRunner implements ApplicationRunner {
|
||||
|
||||
private static final String SEED_CATALOG_VERSION = "seed-20260719-v1";
|
||||
private static final String MARKET_CODE = "LEISURE";
|
||||
private static final String SOURCE_CODE = "TRAVEL_AGENT";
|
||||
|
||||
private static final List<AccountSeed> ACCOUNT_SEEDS = List.of(
|
||||
new AccountSeed("QBD_TRAVEL", "Q.B.D. TRAVEL GROUP CO., LTD"),
|
||||
new AccountSeed("LIAN_TAI", "LIAN TAI TRAVEL (THAILAND) CO., LTD."),
|
||||
new AccountSeed("HANATOUR_TD", "HANATOUR TD CO., LTD."));
|
||||
|
||||
private static final List<CodeSeed> CODE_SEEDS = List.of(
|
||||
new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "TWN", "TWN", 10, "{\"adult_capacity\":2}"),
|
||||
new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "KING", "KING", 20, "{\"adult_capacity\":2}"),
|
||||
new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "DBL", "DBL", 30, "{\"adult_capacity\":2}"),
|
||||
new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "SGL", "SGL", 40, "{\"adult_capacity\":1}"),
|
||||
new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "TRP", "TRP", 50, "{\"adult_capacity\":3}"),
|
||||
new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "RM1", "RM1", 60, "{\"adult_capacity\":2}"),
|
||||
new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "RM2", "RM2", 70, "{\"adult_capacity\":2}"),
|
||||
new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "RM3", "RM3", 80, "{\"adult_capacity\":2}"),
|
||||
new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "BAR", "BAR", 10, "{\"pricing_available\":false}"),
|
||||
new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "RACK", "RACK", 20, "{\"pricing_available\":false}"),
|
||||
new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "PACKAGE", "PACKAGE", 30, "{\"pricing_available\":false}"),
|
||||
new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GROUP", "GROUP", 40, "{\"pricing_available\":false}"),
|
||||
new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "FIT", "FIT", 50, "{\"pricing_available\":false}"),
|
||||
new CodeSeed(ReservationV4CatalogType.MARKET.name(), MARKET_CODE, MARKET_CODE, 10, null),
|
||||
new CodeSeed(ReservationV4CatalogType.SOURCE.name(), SOURCE_CODE, SOURCE_CODE, 10, null));
|
||||
|
||||
private final PlatformHotelRepository hotelRepository;
|
||||
private final ReservationV4CatalogRepository catalogRepository;
|
||||
|
||||
/**
|
||||
* 注入平台酒店 Repository 和 Reservation 目录 Repository,保持平台模块不反向依赖业务目录。
|
||||
*/
|
||||
public ReservationV4CatalogBootstrapRunner(
|
||||
PlatformHotelRepository hotelRepository,
|
||||
ReservationV4CatalogRepository catalogRepository) {
|
||||
this.hotelRepository = hotelRepository;
|
||||
this.catalogRepository = catalogRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用启动时为当前 ACTIVE 酒店补齐初始化目录;已有目录的酒店不会被覆盖。
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(ApplicationArguments args) {
|
||||
List<PlatformHotelEntity> activeHotels = hotelRepository.listActiveHotels();
|
||||
if (activeHotels == null || activeHotels.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
|
||||
for (PlatformHotelEntity hotel : activeHotels) {
|
||||
seedHotelCatalogIfEmpty(hotel == null ? null : hotel.getHotelId(), now);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前酒店目录为空时插入固定初始化目录,避免 Flyway 之后新建的默认酒店没有可用 lookup。
|
||||
*/
|
||||
private void seedHotelCatalogIfEmpty(String hotelId, LocalDateTime now) {
|
||||
String normalizedHotelId = trimToNull(hotelId);
|
||||
if (normalizedHotelId == null) {
|
||||
return;
|
||||
}
|
||||
if (!catalogRepository.hasAnyAccount(normalizedHotelId)) {
|
||||
for (AccountSeed seed : ACCOUNT_SEEDS) {
|
||||
catalogRepository.insertAccountIfAbsent(accountEntity(normalizedHotelId, seed, now));
|
||||
}
|
||||
}
|
||||
if (!catalogRepository.hasAnyCatalogCode(normalizedHotelId)) {
|
||||
for (CodeSeed seed : CODE_SEEDS) {
|
||||
catalogRepository.insertCatalogCodeIfAbsent(codeEntity(normalizedHotelId, seed, now));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 Account 固定种子实体,主键交给 MyBatis-Plus 生成。
|
||||
*/
|
||||
private ReservationCatalogAccountEntity accountEntity(String hotelId, AccountSeed seed, LocalDateTime now) {
|
||||
ReservationCatalogAccountEntity entity = new ReservationCatalogAccountEntity();
|
||||
entity.setHotelId(hotelId);
|
||||
entity.setAccountCode(seed.accountCode());
|
||||
entity.setAccountName(seed.accountName());
|
||||
entity.setMarketCode(MARKET_CODE);
|
||||
entity.setSourceCode(SOURCE_CODE);
|
||||
entity.setStatus(ReservationV4CatalogStatus.ACTIVE.name());
|
||||
entity.setSourceSystem(ReservationV4CatalogSourceSystem.FIXED_SEED_IMPORT.name());
|
||||
entity.setCatalogVersion(SEED_CATALOG_VERSION);
|
||||
entity.setVersion(0L);
|
||||
entity.setCreatedAt(now);
|
||||
entity.setUpdatedAt(now);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造通用代码固定种子实体,主键交给 MyBatis-Plus 生成。
|
||||
*/
|
||||
private ReservationCatalogCodeEntity codeEntity(String hotelId, CodeSeed seed, LocalDateTime now) {
|
||||
ReservationCatalogCodeEntity entity = new ReservationCatalogCodeEntity();
|
||||
entity.setHotelId(hotelId);
|
||||
entity.setCatalogType(seed.catalogType());
|
||||
entity.setCode(seed.code());
|
||||
entity.setDisplayName(seed.displayName());
|
||||
entity.setStatus(ReservationV4CatalogStatus.ACTIVE.name());
|
||||
entity.setSourceSystem(ReservationV4CatalogSourceSystem.FIXED_SEED_IMPORT.name());
|
||||
entity.setSortOrder(seed.sortOrder());
|
||||
entity.setCatalogVersion(SEED_CATALOG_VERSION);
|
||||
entity.setMetadataJson(seed.metadataJson());
|
||||
entity.setVersion(0L);
|
||||
entity.setCreatedAt(now);
|
||||
entity.setUpdatedAt(now);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除字符串首尾空白,空白值统一视为 null。
|
||||
*/
|
||||
private String trimToNull(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private record AccountSeed(String accountCode, String accountName) {
|
||||
}
|
||||
|
||||
private record CodeSeed(String catalogType, String code, String displayName, int sortOrder, String metadataJson) {
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ public class ReservationV4CatalogLookupServiceImpl implements ReservationV4Catal
|
||||
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 int METADATA_PAGE_SIZE = 1;
|
||||
private static final String EMPTY_CATALOG_SOURCE = "DATABASE_EMPTY";
|
||||
private static final String EMPTY_CATALOG_VERSION = "unknown";
|
||||
|
||||
@@ -62,15 +63,20 @@ public class ReservationV4CatalogLookupServiceImpl implements ReservationV4Catal
|
||||
List<ReservationV4CatalogLookupItemResult> items = page.items().stream()
|
||||
.map(this::accountItem)
|
||||
.toList();
|
||||
List<ReservationV4CatalogAccountSnapshot> metadataItems = catalogRepository.queryActiveAccounts(
|
||||
hotelId,
|
||||
null,
|
||||
DEFAULT_PAGE_NUM,
|
||||
METADATA_PAGE_SIZE).items();
|
||||
return new ReservationV4CatalogLookupResult(
|
||||
hotelId,
|
||||
ReservationV4CatalogType.ACCOUNT.name(),
|
||||
sourceSystem(page.items()),
|
||||
catalogVersion(page.items()),
|
||||
sourceSystem(metadataItems),
|
||||
catalogVersion(metadataItems),
|
||||
false,
|
||||
items,
|
||||
new ReservationPaginationResult(page.pageNum(), page.pageSize(), page.total()),
|
||||
warnings(page.items()));
|
||||
warnings(metadataItems));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,15 +113,21 @@ public class ReservationV4CatalogLookupServiceImpl implements ReservationV4Catal
|
||||
List<ReservationV4CatalogLookupItemResult> items = page.items().stream()
|
||||
.map(snapshot -> codeItem(catalogType, snapshot))
|
||||
.toList();
|
||||
List<ReservationV4CatalogCodeSnapshot> metadataItems = catalogRepository.queryActiveCatalogCodes(
|
||||
hotelId,
|
||||
catalogType.name(),
|
||||
null,
|
||||
DEFAULT_PAGE_NUM,
|
||||
METADATA_PAGE_SIZE).items();
|
||||
return new ReservationV4CatalogLookupResult(
|
||||
hotelId,
|
||||
catalogType.name(),
|
||||
sourceSystem(page.items()),
|
||||
catalogVersion(page.items()),
|
||||
sourceSystem(metadataItems),
|
||||
catalogVersion(metadataItems),
|
||||
false,
|
||||
items,
|
||||
new ReservationPaginationResult(page.pageNum(), page.pageSize(), page.total()),
|
||||
warnings(page.items()));
|
||||
warnings(metadataItems));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user