feat:机器核销
This commit is contained in:
1 parent
89384d07f4
commit
bfd1659bdb
16 files changed
+339
-4
No files matched your search
+27
@@ -0,0 +1,27 @@
|
||||
package com.bsd.pjcloud.hard.api.dto.hard;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 预约菜单单笔核销参数。
|
||||
*
|
||||
* 核销目标只能由订单 ID 确定,禁止客户端附带日期、餐段、设备或识别身份参与订单选择。
|
||||
*/
|
||||
@Data
|
||||
public class ReservationMenuCancelDto {
|
||||
|
||||
@NotNull(message = "预约订单ID不能为空")
|
||||
@Schema(description = "待核销预约订单ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long orderId;
|
||||
|
||||
/**
|
||||
* Spring 默认可能忽略未知 JSON 字段,这里显式拒绝,保证请求体只能包含 orderId。
|
||||
*/
|
||||
@JsonAnySetter
|
||||
public void rejectUnknownField(String fieldName, Object value) {
|
||||
throw new IllegalArgumentException("预约核销接口不支持参数:" + fieldName);
|
||||
}
|
||||
}
|
||||
+3
@@ -39,5 +39,8 @@ public class ReservationMenuQueryVo {
|
||||
private String mealTitle;
|
||||
|
||||
private List<String> items;
|
||||
|
||||
/** 同一日期餐段可能合并多笔预约,终端按订单逐笔确认核销。 */
|
||||
private List<Long> orderIds;
|
||||
}
|
||||
}
|
||||
+7
@@ -178,6 +178,13 @@ public class HardApiController {
|
||||
return R.ok(reservationMenuQueryService.query(dto));
|
||||
}
|
||||
|
||||
@PostMapping("/reservation-menu/cancel")
|
||||
@Operation(summary = "按订单ID核销预约订单")
|
||||
@Idempotent(expireTime = 1, key = "'/hard-api/ad/reservation-menu/cancel:' + #dto.getOrderId()", info = "正在核销中,请勿重复操作")
|
||||
public R<Boolean> cancelReservationMenu(@Valid @RequestBody ReservationMenuCancelDto dto) {
|
||||
return R.ok(reservationMenuQueryService.cancel(dto));
|
||||
}
|
||||
|
||||
@GetMapping("/chart-latitude")
|
||||
@Operation(summary = "统计每天、每周、每月、每年的交易人数和次数")
|
||||
public R<HardChartLatitudeVo> chartLatitude(@RequestParam Long merchantId) {
|
||||
|
||||
+6
@@ -1,5 +1,6 @@
|
||||
package com.bsd.pjcloud.hard.service;
|
||||
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuCancelDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuQueryDto;
|
||||
import com.bsd.pjcloud.hard.api.vo.android.ReservationMenuQueryVo;
|
||||
|
||||
@@ -9,4 +10,9 @@ import com.bsd.pjcloud.hard.api.vo.android.ReservationMenuQueryVo;
|
||||
public interface ReservationMenuQueryService {
|
||||
|
||||
ReservationMenuQueryVo query(ReservationMenuQueryDto dto);
|
||||
|
||||
/**
|
||||
* 仅按订单 ID 核销一笔预约订单。
|
||||
*/
|
||||
Boolean cancel(ReservationMenuCancelDto dto);
|
||||
}
|
||||
+35
-2
@@ -13,9 +13,12 @@ import com.bsd.pjcloud.hard.api.constant.order.HardHourTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.refund.HardRefundStatusEnum;
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.HandCancelCodeDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuCancelDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuQueryDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity;
|
||||
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.android.HardAdUserVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.android.ReservationMenuQueryVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.order.HardCarVo;
|
||||
@@ -73,6 +76,33 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
|
||||
return aggregate(recognizedUser, terminalMerchant, orders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean cancel(ReservationMenuCancelDto dto) {
|
||||
HardOrderEntity order = hardOrderService.getById(dto.getOrderId());
|
||||
if (order == null) throw new RuntimeException("预约订单不存在");
|
||||
if (!Objects.equals(order.getOrderType(), HardOrderTypeEnum.RESERVE.getType()) || !Objects.equals(order.getOrderStatus(), HardOrderStatusEnum.PAY.getType())) {
|
||||
throw new RuntimeException("该预约订单当前不可核销");
|
||||
}
|
||||
if (!isCancelableRefundStatus(order.getRefundStatus())) {
|
||||
throw new RuntimeException("该预约订单正在退款或已退款,不能核销");
|
||||
}
|
||||
if (order.getApptUseDate() == null || order.getApptUseDate().isBefore(LocalDate.now(BUSINESS_ZONE))) {
|
||||
throw new RuntimeException("该预约订单已失效");
|
||||
}
|
||||
|
||||
HandCancelCodeDto cancelDto = new HandCancelCodeDto();
|
||||
cancelDto.setOrderId(order.getId());
|
||||
// 当前接口契约只接收订单 ID;-1 沿用商户端和后台手动核销的非终端设备标识。
|
||||
cancelDto.setHardwareId(-1L);
|
||||
return hardOrderService.handCancelCode(cancelDto);
|
||||
}
|
||||
|
||||
private static boolean isCancelableRefundStatus(String refundStatus) {
|
||||
return StrUtil.isBlank(refundStatus)
|
||||
|| Objects.equals(refundStatus, HardRefundStatusEnum.NONE.getStatus())
|
||||
|| Objects.equals(refundStatus, HardRefundStatusEnum.REJECT.getStatus());
|
||||
}
|
||||
|
||||
private TerminalMerchant resolveTerminalMerchant(Long hardId) {
|
||||
HardManagerEntity hard = hardManagerService.getById(hardId);
|
||||
if (hard == null) {
|
||||
@@ -126,6 +156,7 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
|
||||
String hourType = resolveHourType(order);
|
||||
LinkedHashMap<String, MealAccumulator> meals = grouped.computeIfAbsent(order.getApptUseDate(), ignored -> new LinkedHashMap<>());
|
||||
MealAccumulator meal = meals.computeIfAbsent(hourType, ignored -> new MealAccumulator(hourType, resolveMealTitle(order, hourType)));
|
||||
if (order.getId() != null) meal.orderIds().add(order.getId());
|
||||
if (order.getCarList() == null) continue;
|
||||
for (HardCarVo car : order.getCarList()) {
|
||||
if (car != null && StrUtil.isNotBlank(car.getProductName())) {
|
||||
@@ -160,6 +191,7 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
|
||||
meal.setHourType(source.hourType());
|
||||
meal.setMealTitle(source.mealTitle());
|
||||
meal.setItems(new ArrayList<>(source.items()));
|
||||
meal.setOrderIds(new ArrayList<>(source.orderIds()));
|
||||
return meal;
|
||||
}
|
||||
|
||||
@@ -199,10 +231,11 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
|
||||
record TerminalMerchant(MchManageVo merchant) {
|
||||
}
|
||||
|
||||
private record MealAccumulator(String hourType, String mealTitle, LinkedHashSet<String> items) {
|
||||
private record MealAccumulator(String hourType, String mealTitle, LinkedHashSet<String> items,
|
||||
LinkedHashSet<Long> orderIds) {
|
||||
|
||||
private MealAccumulator(String hourType, String mealTitle) {
|
||||
this(hourType, mealTitle, new LinkedHashSet<>());
|
||||
this(hourType, mealTitle, new LinkedHashSet<>(), new LinkedHashSet<>());
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -5,6 +5,8 @@ import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.baomidou.lock.LockInfo;
|
||||
import com.baomidou.lock.LockTemplate;
|
||||
import com.baomidou.lock.annotation.Lock4j;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -150,6 +152,8 @@ import java.util.stream.Collectors;
|
||||
public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrderEntity> implements HardOrderService {
|
||||
|
||||
static private final String TAG = "订单";
|
||||
private static final long RESERVATION_SLOT_LOCK_EXPIRE_MILLIS = 60_000L;
|
||||
private static final long RESERVATION_SLOT_LOCK_WAIT_MILLIS = 3_000L;
|
||||
|
||||
private final HardManagerService hardManagerService;
|
||||
private final HardCarService hardCarService;
|
||||
@@ -189,6 +193,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
private final HardManagerMapper hardManagerMapper;
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final LockTemplate lockTemplate;
|
||||
private final HardThirdMiOrderMapper hardThirdMiOrderMapper;
|
||||
private final RemoteCanteenDeliveryOrderService remoteCanteenDeliveryOrderService;
|
||||
private final YyxService yyxService;
|
||||
@@ -966,6 +971,8 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
}
|
||||
Map<Long, Integer> inventorySnapshot = validateReserveBatchInventory(sourceParentCar.getMerchantId(), sourceCarList, dinerUserIds.size());
|
||||
String subject = buildReserveOrderSubject(dto.getCarParentId(), sourceCarList);
|
||||
// 在所有远程预检完成后再占用餐段锁,缩短并发请求等待时间;锁仍覆盖查询、落单与事务提交。
|
||||
validateSingleReservationOnlyPerMeal(merchant, dinerUserIds, reservation);
|
||||
|
||||
List<HardOrderEntity> createdOrders = new ArrayList<>();
|
||||
try {
|
||||
@@ -1067,6 +1074,83 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开关开启时,同一商户、用餐人、预约日期和餐段只允许存在一笔已支付未核销订单。
|
||||
* 分布式锁按用餐人 ID 排序获取,并在事务完成后释放,避免并发请求同时通过查询后重复落单。
|
||||
*/
|
||||
private void validateSingleReservationOnlyPerMeal(MchManageVo merchant, List<Long> dinerUserIds,
|
||||
HardApptReservationService.ReservationValidation reservation) {
|
||||
MchManageEntity.TableReservationConf conf = merchant.getTableReservationConf();
|
||||
if (conf == null || !Boolean.TRUE.equals(conf.getSingleReservationOnlyPerMeal())) {
|
||||
return;
|
||||
}
|
||||
List<LockInfo> locks = acquireReservationSlotLocks(merchant.getId(), dinerUserIds, reservation);
|
||||
try {
|
||||
registerReservationSlotLockRelease(locks);
|
||||
} catch (RuntimeException exception) {
|
||||
releaseReservationSlotLocks(locks);
|
||||
throw exception;
|
||||
}
|
||||
long existingCount = baseMapper.selectCount(new LambdaQueryWrapper<HardOrderEntity>()
|
||||
.eq(HardOrderEntity::getMerchantId, merchant.getId())
|
||||
.in(HardOrderEntity::getUserId, dinerUserIds)
|
||||
.eq(HardOrderEntity::getSysType, HardTypeEnum.CANTEEN.getType())
|
||||
.eq(HardOrderEntity::getOrderType, HardOrderTypeEnum.RESERVE.getType())
|
||||
.eq(HardOrderEntity::getOrderStatus, HardOrderStatusEnum.PAY.getType())
|
||||
.eq(HardOrderEntity::getApptUseDate, reservation.apptDate())
|
||||
.eq(HardOrderEntity::getHourType, reservation.hourType()));
|
||||
if (existingCount > 0) {
|
||||
throw new RuntimeException("同一用餐人同一天同一餐段只能预约一次,请勿重复预约");
|
||||
}
|
||||
}
|
||||
|
||||
private List<LockInfo> acquireReservationSlotLocks(Long merchantId, List<Long> dinerUserIds,
|
||||
HardApptReservationService.ReservationValidation reservation) {
|
||||
List<LockInfo> locks = new ArrayList<>();
|
||||
try {
|
||||
for (Long dinerUserId : dinerUserIds.stream().sorted().toList()) {
|
||||
String lockKey = String.format("hard:reserve:meal-slot:%s:%s:%s:%s",
|
||||
merchantId, dinerUserId, reservation.apptDate(), reservation.hourType());
|
||||
LockInfo lock = lockTemplate.lock(lockKey, RESERVATION_SLOT_LOCK_EXPIRE_MILLIS,
|
||||
RESERVATION_SLOT_LOCK_WAIT_MILLIS);
|
||||
if (lock == null) {
|
||||
throw new RuntimeException("预约正在处理中,请勿重复提交");
|
||||
}
|
||||
locks.add(lock);
|
||||
}
|
||||
return locks;
|
||||
} catch (RuntimeException exception) {
|
||||
releaseReservationSlotLocks(locks);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private void registerReservationSlotLockRelease(List<LockInfo> locks) {
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
throw new RuntimeException("预约事务状态异常,请稍后重试");
|
||||
}
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCompletion(int status) {
|
||||
releaseReservationSlotLocks(locks);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void releaseReservationSlotLocks(List<LockInfo> locks) {
|
||||
ListIterator<LockInfo> iterator = locks.listIterator(locks.size());
|
||||
while (iterator.hasPrevious()) {
|
||||
LockInfo lock = iterator.previous();
|
||||
try {
|
||||
if (!lockTemplate.releaseLock(lock)) {
|
||||
log.warn("|{}|预约餐段锁释放失败,lockKey:|{}|", TAG, lock.getLockKey());
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
log.error("|{}|预约餐段锁释放异常,lockKey:|{}|", TAG, lock.getLockKey(), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多人复制购物车前先拒绝缺少商品、数量或金额的异常明细,避免在库存汇总阶段出现半途失败。
|
||||
*/
|
||||
|
||||
+3
-2
@@ -14,7 +14,8 @@ public final class HardApptCycleRuleCalculator {
|
||||
|
||||
public static final int MIN_WEEKDAY = DayOfWeek.MONDAY.getValue();
|
||||
public static final int MAX_WEEKDAY = DayOfWeek.SUNDAY.getValue();
|
||||
public static final int MIN_TARGET_OFFSET_DAYS = 1;
|
||||
// 第0天表示截止日期当天,周期规则允许从本批截止日开始生成用餐日期。
|
||||
public static final int MIN_TARGET_OFFSET_DAYS = 0;
|
||||
public static final int MAX_TARGET_OFFSET_DAYS = 31;
|
||||
public static final int MIN_TARGET_DAY_COUNT = 1;
|
||||
public static final int MAX_TARGET_DAY_COUNT = 31;
|
||||
@@ -39,7 +40,7 @@ public final class HardApptCycleRuleCalculator {
|
||||
if (rule.getTargetStartOffsetDays() == null
|
||||
|| rule.getTargetStartOffsetDays() < MIN_TARGET_OFFSET_DAYS
|
||||
|| rule.getTargetStartOffsetDays() > MAX_TARGET_OFFSET_DAYS) {
|
||||
throw new RuntimeException("目标起始天数必须在1至31天之间");
|
||||
throw new RuntimeException("目标起始天数必须在0至31天之间");
|
||||
}
|
||||
if (rule.getTargetDayCount() == null
|
||||
|| rule.getTargetDayCount() < MIN_TARGET_DAY_COUNT
|
||||
|
||||
+18
@@ -90,6 +90,18 @@ class HardApptReservationServiceImplTest {
|
||||
LocalDate.of(2026, 8, 4), null, fridayOpen));
|
||||
}
|
||||
|
||||
@Test
|
||||
void configurableCycleSupportsTargetDateOnCloseDay() {
|
||||
HardApptCycleRuleVo rule = cycleRule(100L, 10L, 5, LocalTime.of(17, 0),
|
||||
7, LocalTime.of(22, 0), 0, 1, "1");
|
||||
HardApptReservationServiceImpl service = reservationService(weeklyAppt(1L, 10L), 0, List.of(rule));
|
||||
LocalDateTime fridayOpen = LocalDateTime.of(2026, 7, 24, 18, 0);
|
||||
|
||||
assertEquals(LocalDate.of(2026, 7, 26),
|
||||
service.validateReservation(10L, null, 100L, "1",
|
||||
LocalDate.of(2026, 7, 26), null, fridayOpen).apptDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void arbitraryWeekdaysAndCrossWeekWindowsAreCalculatedFromConfiguration() {
|
||||
HardApptCycleRuleVo mondayRule = cycleRule(100L, 10L, 1, LocalTime.of(8, 0),
|
||||
@@ -131,6 +143,12 @@ class HardApptReservationServiceImplTest {
|
||||
assertEquals(LocalDate.of(2026, 7, 21), batch.targetStartDate());
|
||||
|
||||
rule.setTargetStartOffsetDays(0);
|
||||
assertDoesNotThrow(() -> HardApptCycleRuleCalculator.validate(rule));
|
||||
HardApptCycleRuleCalculator.CycleBatch sameDayBatch = HardApptCycleRuleCalculator.resolveBatch(
|
||||
rule, LocalDateTime.of(2026, 7, 20, 8, 30));
|
||||
assertEquals(LocalDate.of(2026, 7, 20), sameDayBatch.targetStartDate());
|
||||
|
||||
rule.setTargetStartOffsetDays(-1);
|
||||
assertThrows(RuntimeException.class, () -> HardApptCycleRuleCalculator.validate(rule));
|
||||
rule.setTargetStartOffsetDays(1);
|
||||
rule.setTargetDayCount(32);
|
||||
|
||||
+76
@@ -10,9 +10,13 @@ import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.hard.api.constant.ReservationRecognitionTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.refund.HardRefundStatusEnum;
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.HandCancelCodeDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuCancelDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.ReservationMenuQueryDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity;
|
||||
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.android.HardAdUserVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.android.ReservationMenuQueryVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.order.HardCarVo;
|
||||
@@ -22,6 +26,8 @@ import com.bsd.pjcloud.hard.service.HardManagerService;
|
||||
import com.bsd.pjcloud.hard.service.HardOrderService;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
@@ -109,6 +115,7 @@ class ReservationMenuQueryServiceImplTest {
|
||||
assertEquals("2", result.getDays().get(0).getMeals().get(0).getHourType());
|
||||
assertEquals("午餐", result.getDays().get(0).getMeals().get(0).getMealTitle());
|
||||
assertEquals(List.of("米饭", "青菜", "红烧肉"), result.getDays().get(0).getMeals().get(0).getItems());
|
||||
assertEquals(List.of(1L, 3L), result.getDays().get(0).getMeals().get(0).getOrderIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -121,6 +128,69 @@ class ReservationMenuQueryServiceImplTest {
|
||||
verifyNoMoreInteractions(hardApiService, hardOrderService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelRejectsNonReservationOrder() {
|
||||
HardOrderEntity order = HardOrderEntity.builder()
|
||||
.id(1L)
|
||||
.orderType(HardOrderTypeEnum.BELOW.getType())
|
||||
.orderStatus(HardOrderStatusEnum.PAY.getType())
|
||||
.apptUseDate(LocalDate.now())
|
||||
.build();
|
||||
when(hardOrderService.getById(1L)).thenReturn(order);
|
||||
ReservationMenuCancelDto cancel = cancel(1L);
|
||||
|
||||
assertThrows(RuntimeException.class, () -> service.cancel(cancel));
|
||||
verify(hardOrderService).getById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelUsesOnlyOrderIdAndDelegatesExistingCancellationFlow() {
|
||||
HardOrderEntity order = HardOrderEntity.builder()
|
||||
.id(1L)
|
||||
.userId(300L)
|
||||
.merchantId(200L)
|
||||
.orderType(HardOrderTypeEnum.RESERVE.getType())
|
||||
.orderStatus(HardOrderStatusEnum.PAY.getType())
|
||||
.refundStatus(HardRefundStatusEnum.NONE.getStatus())
|
||||
.apptUseDate(LocalDate.now().plusDays(1))
|
||||
.build();
|
||||
when(hardOrderService.getById(1L)).thenReturn(order);
|
||||
when(hardOrderService.handCancelCode(any())).thenReturn(true);
|
||||
|
||||
assertTrue(service.cancel(cancel(1L)));
|
||||
ArgumentCaptor<HandCancelCodeDto> cancelCaptor = ArgumentCaptor.forClass(HandCancelCodeDto.class);
|
||||
verify(hardOrderService).handCancelCode(cancelCaptor.capture());
|
||||
assertEquals(1L, cancelCaptor.getValue().getOrderId());
|
||||
assertEquals(-1L, cancelCaptor.getValue().getHardwareId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelRejectsOrderDuringRefund() {
|
||||
HardOrderEntity order = HardOrderEntity.builder()
|
||||
.id(1L)
|
||||
.userId(300L)
|
||||
.merchantId(200L)
|
||||
.orderType(HardOrderTypeEnum.RESERVE.getType())
|
||||
.orderStatus(HardOrderStatusEnum.PAY.getType())
|
||||
.refundStatus(HardRefundStatusEnum.APPLY.getStatus())
|
||||
.apptUseDate(LocalDate.now())
|
||||
.build();
|
||||
when(hardOrderService.getById(1L)).thenReturn(order);
|
||||
|
||||
assertThrows(RuntimeException.class, () -> service.cancel(cancel(1L)));
|
||||
verify(hardOrderService).getById(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelRequestRejectsFieldsOtherThanOrderId() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
ReservationMenuCancelDto cancel = objectMapper.readValue("{\"orderId\":1}", ReservationMenuCancelDto.class);
|
||||
assertEquals(1L, cancel.getOrderId());
|
||||
assertThrows(JsonMappingException.class,
|
||||
() -> objectMapper.readValue("{\"orderId\":1,\"hardId\":2}", ReservationMenuCancelDto.class));
|
||||
}
|
||||
|
||||
private void stubTerminal(Long hardId, Long merchantId, String merchantName) {
|
||||
HardManagerEntity hard = new HardManagerEntity();
|
||||
hard.setId(hardId);
|
||||
@@ -166,6 +236,12 @@ class ReservationMenuQueryServiceImplTest {
|
||||
return query;
|
||||
}
|
||||
|
||||
private static ReservationMenuCancelDto cancel(Long orderId) {
|
||||
ReservationMenuCancelDto cancel = new ReservationMenuCancelDto();
|
||||
cancel.setOrderId(orderId);
|
||||
return cancel;
|
||||
}
|
||||
|
||||
private static HardOrderVo order(Long id, LocalDate date, String hourType, String title, LocalDateTime createTime, String... items) {
|
||||
HardOrderVo order = new HardOrderVo();
|
||||
order.setId(id);
|
||||
|
||||
+10
@@ -177,6 +177,16 @@ public class SysUserFaceController {
|
||||
dto.setProcessStatus(TaskCheckStatusEnum.RESULT.getCode());
|
||||
return R.ok(sysUserFaceService.pageJoin(page, dto).getRecords());
|
||||
}
|
||||
|
||||
@Operation(summary = "android查询人脸数据库", description = "android查询人脸数据库")
|
||||
@GetMapping("/ad/v2/find-face")
|
||||
public R adV2FinFace(@ParameterObject Page page, @RequestParam(required = false) Long userId, @RequestParam(required = false) Long merchantDeptId) {
|
||||
SysUserFaceDto dto = new SysUserFaceDto();
|
||||
dto.setUserId(userId);
|
||||
dto.setMerchantDeptIdOrGlobal(merchantDeptId);
|
||||
dto.setProcessStatus(TaskCheckStatusEnum.RESULT.getCode());
|
||||
return R.ok(sysUserFaceService.allPageJoin(page, dto).getRecords());
|
||||
}
|
||||
/* ************************************* start android start ************************************* */
|
||||
|
||||
@Operation(summary = "设置为全局人脸", description = "设置为全局人脸")
|
||||
|
||||
+2
@@ -84,4 +84,6 @@ public interface SysUserFaceMapper extends PjcloudBaseMapper<SysUserFaceEntity>
|
||||
}
|
||||
return selectJoinPage(page, SysUserFaceVo.class, wrapper);
|
||||
}
|
||||
|
||||
Page<SysUserFaceVo> allPageJoin(Page page, @Param("pa") SysUserFaceDto pa);
|
||||
}
|
||||
+2
@@ -54,6 +54,8 @@ public interface SysUserFaceService extends IService<SysUserFaceEntity> {
|
||||
|
||||
Page<SysUserFaceVo> pageJoin(Page page, SysUserFaceDto dto);
|
||||
|
||||
Page<SysUserFaceVo> allPageJoin(Page page, SysUserFaceDto dto);
|
||||
|
||||
SysUserFaceVo join(Long id);
|
||||
|
||||
/**
|
||||
|
||||
+13
@@ -413,6 +413,19 @@ public class SysUserFaceServiceImpl extends ServiceImpl<SysUserFaceMapper, SysUs
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<SysUserFaceVo> allPageJoin(Page page, SysUserFaceDto dto) {
|
||||
if (ObjectUtil.isNotEmpty(dto.getMerchantDeptIdOrGlobal())) {
|
||||
List<SysDept> deptList = sysDeptService.listDescendant(dto.getMerchantDeptIdOrGlobal());
|
||||
if (ObjectUtil.isNotEmpty(deptList)) {
|
||||
List<Long> deptIds = deptList.stream().map(SysDept::getDeptId).toList();
|
||||
dto.setDeptIds(deptIds);
|
||||
}
|
||||
}
|
||||
return baseMapper.allPageJoin(page, dto);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public SysUserFaceVo join(Long id) {
|
||||
return baseMapper.join(id);
|
||||
|
||||
@@ -95,4 +95,50 @@
|
||||
</where>
|
||||
GROUP BY a.user_id
|
||||
</select>
|
||||
|
||||
<select id="allPageJoin" resultType="com.bsd.pjcloud.admin.api.vo.SysUserFaceVo">
|
||||
<include refid="selectA"/>
|
||||
u.name as username,
|
||||
u.phone as userMobile,
|
||||
u.avatar as userAvatar,
|
||||
d.dept_id as deptId,
|
||||
d.name as deptName
|
||||
from sys_user_face a
|
||||
inner join sys_user u on u.user_id = a.user_id and u.del_flag = '0'
|
||||
inner join sys_dept d on d.dept_id = u.dept_id and d.del_flag = '0'
|
||||
<where>
|
||||
<if test="pa.userId != null">
|
||||
and a.user_id = #{pa.userId}
|
||||
</if>
|
||||
<if test="pa.userIds != null and pa.userIds.size() != 0">
|
||||
and a.user_id in
|
||||
<foreach collection="pa.userIds" item="v" open="(" close=")" separator=",">
|
||||
#{v}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="pa.processStatus != null">
|
||||
and a.process_status = #{pa.processStatus}
|
||||
</if>
|
||||
<if test="pa.isProcessIdNotNull != null and pa.isProcessIdNotNull == true ">
|
||||
and a.process_id is not null
|
||||
</if>
|
||||
<if test="pa.deptIds != null and pa.deptIds.size() != 0">
|
||||
<choose>
|
||||
<when test="pa.isInclude != null and pa.isInclude == true">
|
||||
and d.dept_id IN
|
||||
</when>
|
||||
<when test="pa.isInclude != null and pa.isInclude == false">
|
||||
and d.dept_id NOT IN
|
||||
</when>
|
||||
<otherwise>
|
||||
and d.dept_id IN
|
||||
</otherwise>
|
||||
</choose>
|
||||
<foreach collection="pa.deptIds" item="v" open="(" close=")" separator=",">
|
||||
#{v}
|
||||
</foreach>
|
||||
</if>
|
||||
</where>
|
||||
order by a.create_time desc, a.id desc
|
||||
</select>
|
||||
</mapper>
|
||||
+5
@@ -241,6 +241,11 @@ public class MchManageEntity extends Model<MchManageEntity> {
|
||||
*/
|
||||
private Boolean singleProductOnlyInCart = false;
|
||||
|
||||
/**
|
||||
* 同一用餐人在同一商户、同一天、同一餐段是否只允许保留一笔未核销预约。
|
||||
*/
|
||||
private Boolean singleReservationOnlyPerMeal = false;
|
||||
|
||||
/**
|
||||
* 允许代订餐的用户关系编码,对应 sys_user_relation_type 字典和 UserRelationTypeEnum。
|
||||
*/
|
||||
|
||||
+2
@@ -198,6 +198,8 @@ public class MchManageServiceImpl extends ServiceImpl<MchManageMapper, MchManage
|
||||
}
|
||||
conf.setReserveMode(reserveMode);
|
||||
}
|
||||
// 历史 JSON 或旧客户端可能未提交该开关,统一按关闭处理,避免 null 被误判为启用。
|
||||
conf.setSingleReservationOnlyPerMeal(Boolean.TRUE.equals(conf.getSingleReservationOnlyPerMeal()));
|
||||
// 配置保存枚举业务编码;历史数据库ID和无效值在再次保存时自动移除。
|
||||
if (ObjectUtil.isNotEmpty(conf.getUserRelationTypeIdList())) {
|
||||
conf.setUserRelationTypeIdList(conf.getUserRelationTypeIdList().stream()
|
||||
|
||||
Reference in new issue
Block a user