Merge remote-tracking branch 'origin/master'

# Conflicts:
#	pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java
This commit is contained in:
zjf committed 2026-09-13 18:11:04 +08:00
commit 92cabb5796
4 files changed
+239 -80

No files matched your search

@@ -1,41 +1,41 @@
package com.bsd.pjcloud.canteen.dto.report;
import lombok.Data;
import java.math.BigDecimal;
/**
* 包厢台账(汇总)行数据。
* <p>
* 导入已改为 {@code CanteenReportBoxExcelBinder} 按规范化表头名称映射,
* 不再依赖 EasyExcel {@code @ExcelProperty}(多值 value 会被当成多级表头而非别名)。
*/
@Data
public class CanteenReportBoxExcel {
private String monthText;
private String bizDate;
private String costCenter;
private String costCenterCode;
private String empNo;
private String empName;
private BigDecimal foodAmount;
private BigDecimal drinkAmount;
private BigDecimal totalAmount;
private String legalEntity;
private String remark1;
private String remark2;
private String park;
}
package com.bsd.pjcloud.canteen.dto.report;
import lombok.Data;
import java.math.BigDecimal;
/**
* 包厢台账(汇总)行数据。
* <p>
* 导入已改为 {@code CanteenReportBoxExcelBinder} 按规范化表头名称映射,
* 不再依赖 EasyExcel {@code @ExcelProperty}(多值 value 会被当成多级表头而非别名)。
*/
@Data
public class CanteenReportBoxExcel {
private String monthText;
private String bizDate;
private String costCenter;
private String costCenterCode;
private String empNo;
private String empName;
private BigDecimal foodAmount;
private BigDecimal drinkAmount;
private BigDecimal totalAmount;
private String legalEntity;
private String remark1;
private String remark2;
private String park;
}
@@ -45,7 +45,7 @@ class CanteenSpuServiceImplTest {
valueOperations = mock(ValueOperations.class);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
CanteenSpuServiceImpl target = new CanteenSpuServiceImpl(redisTemplate, null, null, null, null, null,
mock(CanteenMerchantScopeHelper.class));
mock(CanteenMerchantScopeHelper.class), mock(CanteenDailyMenuStockSupport.class));
ReflectionTestUtils.setField(target, "baseMapper", spuMapper);
service = target;
TransactionSynchronizationManager.initSynchronization();
@@ -2222,10 +2222,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
}
for (Map.Entry<Long, Integer> entry : required.entrySet()) {
DailyMenuAppVO row = menuBySpu.get(entry.getKey());
// 线上剩余:优先 remainOnlineStock;兼容旧字段 stock/onlineStock(历史把剩余写在配额字段)
int onlineRemain = row.getRemainOnlineStock() != null ? row.getRemainOnlineStock()
: (row.getStock() != null ? row.getStock()
: (row.getOnlineStock() == null ? 0 : row.getOnlineStock()));
int onlineRemain = dailyOnlineRemain(row);
if (onlineRemain < entry.getValue()) {
throw new I18nBizException(ErrorCodes.HARD_PRODUCT_STOCK_LOW, row.getName());
}
@@ -4647,9 +4644,9 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
HardParentCarEntity parentCar = hardCarService.getParentCarById(order.getParentCarId());
boolean dailyMenu = isDailyMenuParentCar(parentCar);
// App 预约(RESERVE)走线上 ONLINE;安卓机等线下订单走 OFFLINE。退款必须与下单同通道,否则剩余对不上。
boolean onlineChannel = dailyMenu
&& Objects.equals(order.getOrderType(), HardOrderTypeEnum.RESERVE.getType());
// 每日菜谱分池路由:线上预定订单下单扣 ONLINE 池(updateDailyMenuReserveInventory),退款/重扣必须还同一池;
// 机器收银等其他类型下单走 OFFLINE 池。不看订单类型会把预约单的线上销量错补到线下池。
boolean onlineChannel = HardOrderTypeEnum.RESERVE.getType().equals(order.getOrderType());
LocalDate mealDate = order.getApptUseDate();
Integer hourType = parseHourType(order.getHourType());
Map<Long, Integer> inventorySnapshot = new LinkedHashMap<>();
@@ -4658,9 +4655,10 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
if (mealDate == null || hourType == null) {
throw new I18nBizException(ErrorCodes.HARD_CANTEEN_SPU_BAD);
}
R<List<DailyMenuAppVO>> menuResult = onlineChannel
? remoteDailyMenuService.listForOrderValidate(mealDate.toString(), hourType, order.getMerchantId())
: remoteDailyMenuService.getCanteenList(mealDate.toString(), hourType, null, order.getMerchantId());
// 库存恢复可能在自动退款定时任务等无用户凭证的上下文执行,必须走 @NoToken 内部菜谱接口;
// getCanteenList 是 AD 终端登录态接口,任务线程调用会被 424 拒绝。
R<List<DailyMenuAppVO>> menuResult = remoteDailyMenuService.listForOrderValidate(
mealDate.toString(), hourType, order.getMerchantId());
if (menuResult == null || !menuResult.isOk() || menuResult.getData() == null) {
throw new I18nBizException(menuResult == null ? ErrorCodes.HARD_CANTEEN_STOCK_NO_RESP
: ErrorCodes.HARD_CANTEEN_STOCK_QUERY_FAIL);
@@ -4674,17 +4672,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
if (row == null) {
throw new I18nBizException(ErrorCodes.HARD_CANTEEN_SPU_BAD);
}
int channelRemain;
if (onlineChannel) {
channelRemain = row.getRemainOnlineStock() != null ? row.getRemainOnlineStock()
: (row.getStock() != null ? row.getStock()
: (row.getOnlineStock() == null ? 0 : row.getOnlineStock()));
}
else {
channelRemain = row.getRemainOfflineStock() != null ? row.getRemainOfflineStock()
: (row.getStock() != null ? row.getStock()
: (row.getOfflineStock() != null ? row.getOfflineStock() : 0));
}
int channelRemain = onlineChannel ? dailyOnlineRemain(row) : dailyOfflineRemain(row);
if (entry.getValue() < 0 && channelRemain < Math.abs(entry.getValue())) {
throw new I18nBizException(ErrorCodes.HARD_PRODUCT_STOCK_LOW, row.getName());
}
@@ -4731,10 +4719,10 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
: "HARD_DAILY_MENU_AFTER_SALE:" + afterSaleId;
if (dailyMenu) {
String stockChannel = onlineChannel ? "ONLINE" : "OFFLINE";
registerCanteenDailyChannelInventoryRollback(stockChanges, inventorySnapshot, resolvedInventoryBizId,
order.getMerchantId(), mealDate, hourType, stockChannel);
applyCanteenDailyChannelInventoryChange(stockChanges, inventorySnapshot, resolvedInventoryBizId, "APPLY",
order.getMerchantId(), mealDate, hourType, stockChannel);
registerCanteenDailyChannelInventoryRollback(stockChannel, stockChanges, inventorySnapshot,
resolvedInventoryBizId, order.getMerchantId(), mealDate, hourType);
applyCanteenDailyChannelInventoryChange(stockChannel, stockChanges, inventorySnapshot,
resolvedInventoryBizId, "APPLY", order.getMerchantId(), mealDate, hourType);
}
else {
registerCanteenSpuInventoryRollback(stockChanges, inventorySnapshot, resolvedInventoryBizId);
@@ -4812,8 +4800,8 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
}
}
/** 每日菜谱分池:按 ONLINE/OFFLINE 改写 daily_menu.*_sold(配额不动)。 */
private void applyCanteenDailyChannelInventoryChange(Map<Long, Integer> stockChanges,
/** 每日菜谱分池幂等变更:ONLINE 改写线上池销量,OFFLINE 改写线下池销量,两池互不影响。 */
private void applyCanteenDailyChannelInventoryChange(String stockChannel, Map<Long, Integer> stockChanges,
Map<Long, Integer> inventorySnapshot, String inventoryBizId, String operation, Long merchantId,
LocalDate mealDate, Integer hourType, String stockChannel) {
CanteenSpuUpdateStockReqDTO dto = new CanteenSpuUpdateStockReqDTO();
@@ -4842,7 +4830,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
}
}
private void registerCanteenDailyChannelInventoryRollback(Map<Long, Integer> stockChanges,
private void registerCanteenDailyChannelInventoryRollback(String stockChannel, Map<Long, Integer> stockChanges,
Map<Long, Integer> inventorySnapshot, String inventoryBizId, Long merchantId, LocalDate mealDate,
Integer hourType, String stockChannel) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
@@ -4858,17 +4846,30 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
return;
}
try {
applyCanteenDailyChannelInventoryChange(rollbackChanges, rollbackSnapshot, inventoryBizId,
"ROLLBACK", merchantId, mealDate, hourType, channel);
applyCanteenDailyChannelInventoryChange(stockChannel, rollbackChanges, rollbackSnapshot, inventoryBizId,
"ROLLBACK", merchantId, mealDate, hourType);
}
catch (Exception compensationException) {
log.error("|{}|每日菜谱{}库存回滚失败,inventoryBizId:|{}|", TAG, channel, inventoryBizId,
compensationException);
log.error("|{}|每日菜谱{}库存回滚失败,inventoryBizId:|{}|", TAG, stockChannel, inventoryBizId, compensationException);
}
}
});
}
/** 线上剩余:优先 remainOnlineStock;兼容旧字段 stock/onlineStock(历史把剩余写在配额字段)。 */
private static int dailyOnlineRemain(DailyMenuAppVO row) {
return row.getRemainOnlineStock() != null ? row.getRemainOnlineStock()
: (row.getStock() != null ? row.getStock()
: (row.getOnlineStock() == null ? 0 : row.getOnlineStock()));
}
/** 线下剩余:优先 remainOfflineStock;兼容旧字段 stock/offlineStock。 */
private static int dailyOfflineRemain(DailyMenuAppVO row) {
return row.getRemainOfflineStock() != null ? row.getRemainOfflineStock()
: (row.getStock() != null ? row.getStock()
: (row.getOfflineStock() != null ? row.getOfflineStock() : 0));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void fastenerInventory(HardOrderEntity order, YesNoEnum isReduce) {
@@ -1,9 +1,12 @@
package com.bsd.pjcloud.hard.service.impl.order;
import com.bsd.pjcloud.canteen.api.feign.RemoteCanteenSpuService;
import com.bsd.pjcloud.canteen.api.feign.RemoteDailyMenuService;
import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
import com.bsd.pjcloud.canteen.vo.app.DailyMenuAppVO;
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
import com.bsd.pjcloud.common.core.exception.ErrorCodes;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.hard.api.constant.HardProductSourceEnum;
import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
@@ -19,10 +22,12 @@ import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.time.LocalDate;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.any;
@@ -32,7 +37,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 机器收银食堂订单库存分流测试,确保 BELOW 订单使用 {@code canteen_spu} 原子库存接口。
* 食堂订单库存分流测试:机器收银 BELOW 订单使用 {@code canteen_spu} 原子库存接口,
* 每日菜谱订单按订单类型路由分池(RESERVE 走 ONLINE,其他类型走 OFFLINE)。
* <p>
* 直接调用五参 {@code fastenerInventory} 重载:四参重载经 getSelf() 需要 Spring 上下文,
* 纯单测环境无法提供;异常文案由全局 i18n 处理器解析,此处只断言错误码。
*/
class HardOrderServiceImplInventoryTest {
@@ -42,6 +51,9 @@ class HardOrderServiceImplInventoryTest {
/** 食堂基础菜品远程服务,负责查询权威库存并执行幂等扣减。 */
private final RemoteCanteenSpuService remoteCanteenSpuService = mock(RemoteCanteenSpuService.class);
/** 每日菜谱远程服务,负责按日期餐段查询分池剩余。 */
private final RemoteDailyMenuService remoteDailyMenuService = mock(RemoteDailyMenuService.class);
/** 被测订单服务,仅执行真实库存方法,其他依赖不参与本组测试。 */
private final HardOrderServiceImpl service = mock(HardOrderServiceImpl.class, CALLS_REAL_METHODS);
@@ -52,6 +64,7 @@ class HardOrderServiceImplInventoryTest {
void setUp() {
ReflectionTestUtils.setField(service, "hardCarService", hardCarService);
ReflectionTestUtils.setField(service, "remoteCanteenSpuService", remoteCanteenSpuService);
ReflectionTestUtils.setField(service, "remoteDailyMenuService", remoteDailyMenuService);
TransactionSynchronizationManager.initSynchronization();
}
@@ -75,9 +88,9 @@ class HardOrderServiceImplInventoryTest {
when(remoteCanteenSpuService.getInnerSpuListByIds(List.of(11L))).thenReturn(R.ok(List.of(product)));
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.fastenerInventory(cashierOrder(), null, YesNoEnum.YES, null));
() -> service.fastenerInventory(cashierOrder(), null, YesNoEnum.YES, null, null));
assertEquals("测试菜品库存不足", exception.getMessage());
assertEquals(ErrorCodes.HARD_PRODUCT_STOCK_LOW, exception.getMessage());
verify(remoteCanteenSpuService, never()).updateSkuStockIdempotent(any());
}
@@ -91,7 +104,7 @@ class HardOrderServiceImplInventoryTest {
when(remoteCanteenSpuService.getInnerSpuListByIds(List.of(11L))).thenReturn(R.ok(List.of(product)));
when(remoteCanteenSpuService.updateSkuStockIdempotent(any())).thenReturn(R.ok());
assertDoesNotThrow(() -> service.fastenerInventory(cashierOrder(), null, YesNoEnum.YES, null));
assertDoesNotThrow(() -> service.fastenerInventory(cashierOrder(), null, YesNoEnum.YES, null, null));
ArgumentCaptor<CanteenSpuUpdateStockReqDTO> captor = ArgumentCaptor.forClass(CanteenSpuUpdateStockReqDTO.class);
verify(remoteCanteenSpuService).updateSkuStockIdempotent(captor.capture());
@@ -129,7 +142,7 @@ class HardOrderServiceImplInventoryTest {
RuntimeException exception = assertThrows(RuntimeException.class, () -> ReflectionTestUtils.invokeMethod(
service, "validateCanteenSpuOrderInventory", cashierOrder()));
assertEquals("测试菜品库存不足", exception.getMessage());
assertEquals(ErrorCodes.HARD_PRODUCT_STOCK_LOW, exception.getMessage());
verify(remoteCanteenSpuService, never()).updateSkuStockIdempotent(any());
}
@@ -143,7 +156,7 @@ class HardOrderServiceImplInventoryTest {
when(remoteCanteenSpuService.getInnerSpuListByIds(List.of(11L))).thenReturn(R.ok(List.of(product)));
when(remoteCanteenSpuService.updateSkuStockIdempotent(any())).thenReturn(R.ok());
assertDoesNotThrow(() -> service.fastenerInventory(cashierOrder(), null, YesNoEnum.NO, 500L));
assertDoesNotThrow(() -> service.fastenerInventory(cashierOrder(), null, YesNoEnum.NO, 500L, null));
ArgumentCaptor<CanteenSpuUpdateStockReqDTO> captor = ArgumentCaptor.forClass(CanteenSpuUpdateStockReqDTO.class);
verify(remoteCanteenSpuService).updateSkuStockIdempotent(captor.capture());
@@ -174,6 +187,96 @@ class HardOrderServiceImplInventoryTest {
assertEquals(-2, request.getItems().get(0).getIncrCount());
}
/**
* 验证设备购买链路(/hard-api/ad/create-order 产生的 BELOW+CANTEEN_SPU 购物车)扣基础 canteen_spu.stock:
* 请求不携带每日菜谱分池通道,也不查询每日菜谱接口——每日菜谱页面的线上/线下剩余不会变化。
*/
@Test
void deviceCreateOrderDeductsBasicSpuStockWithoutDailyMenuPools() {
prepareCart(1);
CanteenSpuEntity product = productWithStock(9996);
when(remoteCanteenSpuService.getInnerSpuListByIds(List.of(11L))).thenReturn(R.ok(List.of(product)));
when(remoteCanteenSpuService.updateSkuStockIdempotent(any())).thenReturn(R.ok());
assertDoesNotThrow(() -> service.fastenerInventory(cashierOrder(), null, YesNoEnum.YES, null, null));
ArgumentCaptor<CanteenSpuUpdateStockReqDTO> captor = ArgumentCaptor.forClass(CanteenSpuUpdateStockReqDTO.class);
verify(remoteCanteenSpuService).updateSkuStockIdempotent(captor.capture());
CanteenSpuUpdateStockReqDTO request = captor.getValue();
assertNull(request.getStockChannel());
assertEquals(-1, request.getItems().get(0).getIncrCount());
verify(remoteDailyMenuService, never()).listForOrderValidate(any(), any(), any());
verify(remoteDailyMenuService, never()).getCanteenList(any(), any(), any(), any());
}
/**
* 验证线上预定每日菜谱订单退款恢复 ONLINE 池:下单扣的是 online_sold,退款必须还同一池,
* 快照取线上剩余并携带日期餐段。
*/
@Test
void reserveRefundRestoresDailyMenuOnlineStock() {
prepareDailyMenuCart(2);
when(remoteDailyMenuService.listForOrderValidate(any(), any(), any()))
.thenReturn(R.ok(List.of(dailyMenuRow(8, 4))));
when(remoteCanteenSpuService.updateSkuStockIdempotent(any())).thenReturn(R.ok());
assertDoesNotThrow(() -> service.fastenerInventory(reserveDailyMenuOrder(), null, YesNoEnum.NO, 500L, null));
ArgumentCaptor<CanteenSpuUpdateStockReqDTO> captor = ArgumentCaptor.forClass(CanteenSpuUpdateStockReqDTO.class);
verify(remoteCanteenSpuService).updateSkuStockIdempotent(captor.capture());
CanteenSpuUpdateStockReqDTO request = captor.getValue();
assertEquals("APPLY", request.getOperation());
assertEquals("ONLINE", request.getStockChannel());
assertEquals("HARD_DAILY_MENU_AFTER_SALE:500", request.getBizId());
assertEquals(300L, request.getMerchantId());
CanteenSpuUpdateStockReqDTO.Item item = request.getItems().get(0);
assertEquals(11L, item.getId());
assertEquals(2, item.getIncrCount());
assertEquals(8, item.getBeforeStock());
assertEquals(LocalDate.of(2026, 9, 13), item.getMealDate());
assertEquals(2, item.getHourType());
}
/**
* 验证机器收银等其他类型的每日菜谱订单退款仍恢复 OFFLINE 池,快照取线下剩余。
*/
@Test
void cashierDailyMenuRefundRestoresOfflineStock() {
prepareDailyMenuCart(2);
when(remoteDailyMenuService.listForOrderValidate(any(), any(), any()))
.thenReturn(R.ok(List.of(dailyMenuRow(8, 4))));
when(remoteCanteenSpuService.updateSkuStockIdempotent(any())).thenReturn(R.ok());
HardOrderEntity order = reserveDailyMenuOrder();
order.setOrderType(HardOrderTypeEnum.BELOW.getType());
assertDoesNotThrow(() -> service.fastenerInventory(order, null, YesNoEnum.NO, 500L, null));
ArgumentCaptor<CanteenSpuUpdateStockReqDTO> captor = ArgumentCaptor.forClass(CanteenSpuUpdateStockReqDTO.class);
verify(remoteCanteenSpuService).updateSkuStockIdempotent(captor.capture());
CanteenSpuUpdateStockReqDTO request = captor.getValue();
assertEquals("OFFLINE", request.getStockChannel());
assertEquals("HARD_DAILY_MENU_AFTER_SALE:500", request.getBizId());
assertEquals(2, request.getItems().get(0).getIncrCount());
assertEquals(4, request.getItems().get(0).getBeforeStock());
}
/**
* 验证线上预定重审重扣按线上剩余校验:线上不足即拒绝,即使线下剩余充足。
*/
@Test
void reserveReoccupyRejectsInsufficientOnlineStock() {
prepareDailyMenuCart(2);
when(remoteDailyMenuService.listForOrderValidate(any(), any(), any()))
.thenReturn(R.ok(List.of(dailyMenuRow(1, 4))));
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.fastenerInventory(reserveDailyMenuOrder(), null, YesNoEnum.YES, 500L,
"HARD_AFTER_SALE_REVIEW_REOCCUPY:500:1"));
assertEquals(ErrorCodes.HARD_PRODUCT_STOCK_LOW, exception.getMessage());
verify(remoteCanteenSpuService, never()).updateSkuStockIdempotent(any());
}
/**
* 准备机器收银购物车来源和商品数量。
*
@@ -193,6 +296,44 @@ class HardOrderServiceImplInventoryTest {
when(hardCarService.getByParentCarId(200L)).thenReturn(List.of(car));
}
/**
* 准备每日菜谱购物车来源(cartScene 与商品来源标记与服务端保存逻辑一致)和商品数量。
*
* @param productCount 本次购买份数
*/
private void prepareDailyMenuCart(int productCount) {
HardParentCarEntity.TakeOutSetting setting = new HardParentCarEntity.TakeOutSetting();
setting.setCartScene("DAILY_MENU");
HardParentCarEntity parentCar = HardParentCarEntity.builder()
.id(200L)
.productSource(HardProductSourceEnum.CANTEEN_DAILY_MENU.getType())
.takeOutSettingJson(setting)
.build();
HardCarEntity car = HardCarEntity.builder()
.parentCarId(200L)
.productId(11L)
.productCount(productCount)
.build();
when(hardCarService.getParentCarById(200L)).thenReturn(parentCar);
when(hardCarService.getByParentCarId(200L)).thenReturn(List.of(car));
}
/**
* 构造指定线上/线下剩余的每日菜谱行。
*
* @param onlineRemain 线上池剩余
* @param offlineRemain 线下池剩余
* @return 用于分池校验的菜谱行
*/
private DailyMenuAppVO dailyMenuRow(int onlineRemain, int offlineRemain) {
DailyMenuAppVO row = new DailyMenuAppVO();
row.setSpuId(11L);
row.setName("测试菜品");
row.setRemainOnlineStock(onlineRemain);
row.setRemainOfflineStock(offlineRemain);
return row;
}
/**
* 构造指定权威库存的当前商户基础菜品。
*
@@ -223,4 +364,21 @@ class HardOrderServiceImplInventoryTest {
.merchantId(300L)
.build();
}
/**
* 构造线上预定提交的每日菜谱订单。
*
* @return 关联每日菜谱购物车、固定日期餐段和商户的 RESERVE 订单
*/
private HardOrderEntity reserveDailyMenuOrder() {
return HardOrderEntity.builder()
.id(100L)
.sysType(HardTypeEnum.CANTEEN.getType())
.orderType(HardOrderTypeEnum.RESERVE.getType())
.parentCarId(200L)
.merchantId(300L)
.apptUseDate(LocalDate.of(2026, 9, 13))
.hourType("2")
.build();
}
}