feat:机器核销

This commit is contained in:
dzc committed 2026-07-27 20:34:40 +08:00
1 parent 89384d07f4
commit bfd1659bdb
16 files changed
+339 -4

No files matched your search

@@ -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);
}
}
@@ -39,5 +39,8 @@ public class ReservationMenuQueryVo {
private String mealTitle;
private List<String> items;
/** 同一日期餐段可能合并多笔预约,终端按订单逐笔确认核销。 */
private List<Long> orderIds;
}
}
@@ -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) {
@@ -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);
}
@@ -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<>());
}
}
}
@@ -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);
}
}
}
/**
* 多人复制购物车前先拒绝缺少商品、数量或金额的异常明细,避免在库存汇总阶段出现半途失败。
*/
@@ -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
@@ -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);
@@ -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);
@@ -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 = "设置为全局人脸")
@@ -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);
}
@@ -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);
/**
@@ -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>
@@ -241,6 +241,11 @@ public class MchManageEntity extends Model<MchManageEntity> {
*/
private Boolean singleProductOnlyInCart = false;
/**
* 同一用餐人在同一商户、同一天、同一餐段是否只允许保留一笔未核销预约。
*/
private Boolean singleReservationOnlyPerMeal = false;
/**
* 允许代订餐的用户关系编码,对应 sys_user_relation_type 字典和 UserRelationTypeEnum。
*/
@@ -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()