|
|
@@ -0,0 +1,667 @@
|
|
|
+package shop.alien.store.service.impl;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+import shop.alien.entity.store.StoreCuisine;
|
|
|
+import shop.alien.entity.store.StoreOrder;
|
|
|
+import shop.alien.entity.store.StoreOrderDetail;
|
|
|
+import shop.alien.entity.store.StoreTable;
|
|
|
+import shop.alien.entity.store.UserReservation;
|
|
|
+import shop.alien.entity.store.UserReservationTable;
|
|
|
+import shop.alien.entity.store.dto.StoreBookingChangeTableDTO;
|
|
|
+import shop.alien.entity.store.dto.StoreBookingPlaceOrderDTO;
|
|
|
+import shop.alien.entity.store.dto.StoreBookingPlaceOrderItemDTO;
|
|
|
+import shop.alien.entity.store.vo.StoreBookingChangeTableResultVo;
|
|
|
+import shop.alien.entity.store.vo.StoreBookingChangeTableUnifiedResultVo;
|
|
|
+import shop.alien.entity.store.vo.StoreBookingPlaceOrderResultVo;
|
|
|
+import shop.alien.mapper.StoreCuisineMapper;
|
|
|
+import shop.alien.mapper.StoreOrderDetailMapper;
|
|
|
+import shop.alien.mapper.StoreOrderMapper;
|
|
|
+import shop.alien.mapper.StoreTableMapper;
|
|
|
+import shop.alien.mapper.UserReservationMapper;
|
|
|
+import shop.alien.mapper.UserReservationTableMapper;
|
|
|
+import shop.alien.store.service.StoreBookingOrderService;
|
|
|
+import shop.alien.store.service.StoreBookingTableService;
|
|
|
+import shop.alien.util.common.JwtUtil;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Comparator;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 预订下单:不依赖 alien-dining,直接写入订单主表与明细表。
|
|
|
+ * <p>
|
|
|
+ * 同一桌已存在待支付订单时,视为「加餐」:仅追加明细(is_add_dish=1),累加总金额,不新建订单号。
|
|
|
+ * 同一待支付订单可多次加餐(多次调用本接口),不限次数。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+@Transactional(rollbackFor = Exception.class)
|
|
|
+public class StoreBookingOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOrder> implements StoreBookingOrderService {
|
|
|
+
|
|
|
+ private final StoreOrderDetailMapper storeOrderDetailMapper;
|
|
|
+ private final StoreTableMapper storeTableMapper;
|
|
|
+ private final StoreCuisineMapper storeCuisineMapper;
|
|
|
+ private final UserReservationMapper userReservationMapper;
|
|
|
+ private final UserReservationTableMapper userReservationTableMapper;
|
|
|
+ private final StoreBookingTableService storeBookingTableService;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public StoreBookingPlaceOrderResultVo placeOrder(StoreBookingPlaceOrderDTO dto) {
|
|
|
+ if (dto.getDiningGender() != null && dto.getDiningGender() != 1 && dto.getDiningGender() != 2) {
|
|
|
+ throw new IllegalArgumentException("联系人性别仅支持:1男 2女");
|
|
|
+ }
|
|
|
+
|
|
|
+ StoreTable table = storeTableMapper.selectById(dto.getTableId());
|
|
|
+ if (table == null || table.getStoreId() == null || !table.getStoreId().equals(dto.getStoreId())) {
|
|
|
+ throw new IllegalArgumentException("桌号不存在或与门店不匹配");
|
|
|
+ }
|
|
|
+
|
|
|
+ StoreOrder pendingOrder = resolvePendingOrder(dto, table);
|
|
|
+
|
|
|
+ Integer userId = null;
|
|
|
+ String userPhone = null;
|
|
|
+ try {
|
|
|
+ JSONObject jwt = JwtUtil.getCurrentUserInfo();
|
|
|
+ if (jwt != null) {
|
|
|
+ userId = jwt.getInteger("userId");
|
|
|
+ userPhone = jwt.getString("phone");
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("placeOrder: 未解析到登录用户,按匿名下单处理");
|
|
|
+ }
|
|
|
+
|
|
|
+ Date now = new Date();
|
|
|
+ BigDecimal linesSubtotal = validateAndSumLines(dto);
|
|
|
+
|
|
|
+ if (pendingOrder != null) {
|
|
|
+ return addDishesToExistingOrder(dto, table, pendingOrder, linesSubtotal, userId, userPhone, now);
|
|
|
+ }
|
|
|
+
|
|
|
+ String orderNo = generateOrderNo();
|
|
|
+
|
|
|
+ StoreOrder order = new StoreOrder();
|
|
|
+ order.setOrderNo(orderNo);
|
|
|
+ order.setStoreId(dto.getStoreId());
|
|
|
+ order.setTableId(dto.getTableId());
|
|
|
+ order.setTableNumber(table.getTableNumber());
|
|
|
+ order.setUserReservationId(dto.getUserReservationId());
|
|
|
+ order.setDinerCount(dto.getDinerCount());
|
|
|
+ order.setContactPhone(StringUtils.hasText(dto.getContactPhone()) ? dto.getContactPhone().trim() : null);
|
|
|
+ order.setDiningContactName(StringUtils.hasText(dto.getDiningContactName()) ? dto.getDiningContactName().trim() : null);
|
|
|
+ order.setDiningGender(dto.getDiningGender());
|
|
|
+ order.setPayUserId(userId);
|
|
|
+ order.setPayUserPhone(userPhone);
|
|
|
+ order.setOrderStatus(0);
|
|
|
+ order.setTotalAmount(linesSubtotal);
|
|
|
+ order.setDiscountAmount(BigDecimal.ZERO);
|
|
|
+ order.setTablewareFee(BigDecimal.ZERO);
|
|
|
+ order.setPayAmount(linesSubtotal);
|
|
|
+ order.setPayStatus(0);
|
|
|
+ order.setRemark(StringUtils.hasText(dto.getRemark()) ? dto.getRemark().trim() : null);
|
|
|
+ order.setCreatedUserId(userId);
|
|
|
+ order.setUpdatedUserId(userId);
|
|
|
+ order.setCreatedTime(now);
|
|
|
+ order.setUpdatedTime(now);
|
|
|
+ this.save(order);
|
|
|
+
|
|
|
+ insertDetailRows(order.getId(), orderNo, dto.getItems(), userId, userPhone, now, false);
|
|
|
+
|
|
|
+ StoreTable tablePatch = new StoreTable();
|
|
|
+ tablePatch.setId(table.getId());
|
|
|
+ tablePatch.setCurrentOrderId(order.getId());
|
|
|
+ tablePatch.setStatus(1);
|
|
|
+ tablePatch.setDinerCount(dto.getDinerCount());
|
|
|
+ storeTableMapper.updateById(tablePatch);
|
|
|
+
|
|
|
+ StoreBookingPlaceOrderResultVo vo = new StoreBookingPlaceOrderResultVo();
|
|
|
+ vo.setOrderId(order.getId());
|
|
|
+ vo.setOrderNo(orderNo);
|
|
|
+ vo.setTotalAmount(linesSubtotal);
|
|
|
+ vo.setAddDish(false);
|
|
|
+ log.info("预订首单成功 orderId={} orderNo={} tableId={} storeId={}", order.getId(), orderNo, dto.getTableId(), dto.getStoreId());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 换桌统一入口:无源桌则只查空闲桌;有源桌则执行迁桌(1×1 / 1×多 / 多×1 / N×N)。
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public StoreBookingChangeTableUnifiedResultVo changeTable(StoreBookingChangeTableDTO dto) {
|
|
|
+ Integer storeId = dto.getStoreId();
|
|
|
+ if (storeId == null) {
|
|
|
+ throw new IllegalArgumentException("门店ID不能为空");
|
|
|
+ }
|
|
|
+ List<Integer> sourceTableIds = dto.getSourceTableIds();
|
|
|
+ if (sourceTableIds == null || sourceTableIds.isEmpty()) {
|
|
|
+ // 仅查询空闲桌(原独立 GET,现合并到本接口)
|
|
|
+ StoreBookingChangeTableUnifiedResultVo out = new StoreBookingChangeTableUnifiedResultVo();
|
|
|
+ out.setIdleTables(storeBookingTableService.listIdleTablesForChange(storeId));
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+ StoreBookingChangeTableUnifiedResultVo out = new StoreBookingChangeTableUnifiedResultVo();
|
|
|
+ out.setChangeResult(executeChangeTable(dto));
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行换桌:按源/目标桌数量关系处理 1×1、1×多、多×1(合并)、N×N 配对,并同步预约时段与桌台占用。
|
|
|
+ * <p>
|
|
|
+ * 约定:多桌合并为一桌时,多笔待支付合并为一笔,保证「多人加餐、一人结账」仅一笔待支付;明细与实付金额累加至主单。
|
|
|
+ */
|
|
|
+ private StoreBookingChangeTableResultVo executeChangeTable(StoreBookingChangeTableDTO dto) {
|
|
|
+ Integer storeId = dto.getStoreId();
|
|
|
+ List<Integer> sourceTableIds = dto.getSourceTableIds();
|
|
|
+ List<Integer> targetTableIds = dto.getTargetTableIds();
|
|
|
+ if (storeId == null) {
|
|
|
+ throw new IllegalArgumentException("门店ID不能为空");
|
|
|
+ }
|
|
|
+ if (dto.getCarryReservationTime() == null) {
|
|
|
+ throw new IllegalArgumentException("执行换桌时请指定是否沿用原预约时间 carryReservationTime");
|
|
|
+ }
|
|
|
+ if (targetTableIds == null || targetTableIds.isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("执行换桌时目标桌台不能为空");
|
|
|
+ }
|
|
|
+ if (new HashSet<>(sourceTableIds).size() != sourceTableIds.size()) {
|
|
|
+ throw new IllegalArgumentException("源桌列表存在重复");
|
|
|
+ }
|
|
|
+ if (new HashSet<>(targetTableIds).size() != targetTableIds.size()) {
|
|
|
+ throw new IllegalArgumentException("目标桌列表存在重复");
|
|
|
+ }
|
|
|
+ for (Integer a : sourceTableIds) {
|
|
|
+ if (targetTableIds.contains(a)) {
|
|
|
+ throw new IllegalArgumentException("源桌与目标桌不能包含相同桌ID");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Integer> src = sourceTableIds.stream().sorted().collect(Collectors.toList());
|
|
|
+ List<Integer> tgt = targetTableIds.stream().sorted().collect(Collectors.toList());
|
|
|
+ int n = src.size();
|
|
|
+ int m = tgt.size();
|
|
|
+
|
|
|
+ Integer userId = null;
|
|
|
+ try {
|
|
|
+ JSONObject jwt = JwtUtil.getCurrentUserInfo();
|
|
|
+ if (jwt != null) {
|
|
|
+ userId = jwt.getInteger("userId");
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("changeTable: 未解析到登录用户");
|
|
|
+ }
|
|
|
+ Date now = new Date();
|
|
|
+
|
|
|
+ // 源桌须属于本店且存在待支付订单;目标桌须空闲且可绑定订单
|
|
|
+ Map<Integer, StoreTable> sourceTableMap = loadAndAssertStoreTables(storeId, src, "源桌");
|
|
|
+ Map<Integer, StoreTable> targetTableMap = loadAndAssertStoreTables(storeId, tgt, "目标桌");
|
|
|
+ for (Integer tid : tgt) {
|
|
|
+ assertTargetTableIdle(targetTableMap.get(tid));
|
|
|
+ }
|
|
|
+
|
|
|
+ LinkedHashMap<Integer, StoreOrder> orderBySourceTable = new LinkedHashMap<>();
|
|
|
+ for (Integer sid : src) {
|
|
|
+ StoreOrder o = findSinglePendingOrderForTable(storeId, sid);
|
|
|
+ if (o == null) {
|
|
|
+ throw new IllegalArgumentException("源桌无待支付订单,tableId=" + sid);
|
|
|
+ }
|
|
|
+ orderBySourceTable.put(sid, o);
|
|
|
+ }
|
|
|
+
|
|
|
+ List<StoreOrder> orders = src.stream().map(orderBySourceTable::get).collect(Collectors.toList());
|
|
|
+ validateReservationConsistency(orders, dto);
|
|
|
+
|
|
|
+ StoreBookingChangeTableResultVo resultVo = new StoreBookingChangeTableResultVo();
|
|
|
+ resultVo.setAffectedOrderIds(orders.stream().map(StoreOrder::getId).collect(Collectors.toList()));
|
|
|
+ resultVo.setMergedIntoPrimaryOrderIds(new ArrayList<>());
|
|
|
+
|
|
|
+ if (n == m) {
|
|
|
+ // N×N(含 1×1):按源桌、目标桌升序一一配对,分别改订单桌号
|
|
|
+ for (int i = 0; i < n; i++) {
|
|
|
+ Integer sId = src.get(i);
|
|
|
+ Integer tId = tgt.get(i);
|
|
|
+ StoreOrder o = orderBySourceTable.get(sId);
|
|
|
+ StoreTable newT = targetTableMap.get(tId);
|
|
|
+ releaseTableIfCurrentOrder(storeTableMapper.selectById(sId), o.getId());
|
|
|
+ patchOrderTable(o, newT, userId, now);
|
|
|
+ assignTableToOrder(newT, o, o.getDinerCount());
|
|
|
+ }
|
|
|
+ Integer reservationId = unifiedReservationId(orders);
|
|
|
+ if (reservationId != null) {
|
|
|
+ replaceReservationTables(reservationId, tgt);
|
|
|
+ }
|
|
|
+ StoreOrder primary = orderBySourceTable.get(src.get(0));
|
|
|
+ resultVo.setPrimaryOrderId(primary.getId());
|
|
|
+ resultVo.setPrimaryOrderNo(primary.getOrderNo());
|
|
|
+ resultVo.setNewTableIds(tgt);
|
|
|
+ resultVo.setMessage("换桌成功(一一配对)");
|
|
|
+ return resultVo;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (n == 1 && m > 1) {
|
|
|
+ // 1×多:一单多桌,订单主表落在第一目标桌,多桌共用 current_order_id 便于各端同步同一笔待支付
|
|
|
+ StoreOrder o = orderBySourceTable.get(src.get(0));
|
|
|
+ StoreTable firstTarget = targetTableMap.get(tgt.get(0));
|
|
|
+ releaseTableIfCurrentOrder(sourceTableMap.get(src.get(0)), o.getId());
|
|
|
+ patchOrderTable(o, firstTarget, userId, now);
|
|
|
+ for (Integer tId : tgt) {
|
|
|
+ assignTableToOrder(targetTableMap.get(tId), o, o.getDinerCount());
|
|
|
+ }
|
|
|
+ Integer reservationId = o.getUserReservationId();
|
|
|
+ if (reservationId != null) {
|
|
|
+ replaceReservationTables(reservationId, tgt);
|
|
|
+ }
|
|
|
+ resultVo.setPrimaryOrderId(o.getId());
|
|
|
+ resultVo.setPrimaryOrderNo(o.getOrderNo());
|
|
|
+ resultVo.setNewTableIds(tgt);
|
|
|
+ resultVo.setMessage("换桌成功(一桌多单台)");
|
|
|
+ return resultVo;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (n > 1 && m == 1) {
|
|
|
+ // 多×1:合并订单至主单(ID 最小),其余订单取消,仅一笔待支付
|
|
|
+ orders.sort(Comparator.comparing(StoreOrder::getId));
|
|
|
+ StoreOrder primary = orders.get(0);
|
|
|
+ List<StoreOrder> secondaries = orders.subList(1, orders.size());
|
|
|
+ StoreTable newT = targetTableMap.get(tgt.get(0));
|
|
|
+
|
|
|
+ for (StoreOrder o : orders) {
|
|
|
+ releaseTableIfCurrentOrder(storeTableMapper.selectById(o.getTableId()), o.getId());
|
|
|
+ }
|
|
|
+ mergePendingOrdersIntoPrimary(primary, secondaries, userId, now);
|
|
|
+ patchOrderTable(primary, newT, userId, now);
|
|
|
+ assignTableToOrder(newT, primary, maxDinerCount(orders));
|
|
|
+
|
|
|
+ Integer reservationId = unifiedReservationId(orders);
|
|
|
+ if (reservationId != null) {
|
|
|
+ replaceReservationTables(reservationId, tgt);
|
|
|
+ }
|
|
|
+ resultVo.setPrimaryOrderId(primary.getId());
|
|
|
+ resultVo.setPrimaryOrderNo(primary.getOrderNo());
|
|
|
+ resultVo.setMergedIntoPrimaryOrderIds(secondaries.stream().map(StoreOrder::getId).collect(Collectors.toList()));
|
|
|
+ resultVo.setNewTableIds(tgt);
|
|
|
+ resultVo.setMessage("换桌成功(多桌合并,已合并为单笔待支付订单)");
|
|
|
+ return resultVo;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new IllegalArgumentException("不支持的换桌组合:源桌数=" + n + ",目标桌数=" + m + ",仅支持 N×N、1×多、多×1");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 多桌合并时取最大就餐人数,避免汇总后人数偏小 */
|
|
|
+ private static int maxDinerCount(List<StoreOrder> orders) {
|
|
|
+ int max = 1;
|
|
|
+ for (StoreOrder o : orders) {
|
|
|
+ if (o.getDinerCount() != null) {
|
|
|
+ max = Math.max(max, o.getDinerCount());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return max;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将多笔待支付合并为一笔:明细归主单、金额累加、从单取消(状态=2),保证结账只处理主单。
|
|
|
+ */
|
|
|
+ private void mergePendingOrdersIntoPrimary(StoreOrder primary, List<StoreOrder> secondaries, Integer userId, Date now) {
|
|
|
+ BigDecimal total = primary.getTotalAmount() != null ? primary.getTotalAmount() : BigDecimal.ZERO;
|
|
|
+ BigDecimal pay = primary.getPayAmount() != null ? primary.getPayAmount() : BigDecimal.ZERO;
|
|
|
+ for (StoreOrder sec : secondaries) {
|
|
|
+ total = total.add(sec.getTotalAmount() != null ? sec.getTotalAmount() : BigDecimal.ZERO);
|
|
|
+ pay = pay.add(sec.getPayAmount() != null ? sec.getPayAmount() : BigDecimal.ZERO);
|
|
|
+
|
|
|
+ LambdaQueryWrapper<StoreOrderDetail> dq = new LambdaQueryWrapper<>();
|
|
|
+ dq.eq(StoreOrderDetail::getOrderId, sec.getId());
|
|
|
+ List<StoreOrderDetail> lines = storeOrderDetailMapper.selectList(dq);
|
|
|
+ for (StoreOrderDetail d : lines) {
|
|
|
+ d.setOrderId(primary.getId());
|
|
|
+ d.setOrderNo(primary.getOrderNo());
|
|
|
+ d.setUpdatedUserId(userId);
|
|
|
+ d.setUpdatedTime(now);
|
|
|
+ storeOrderDetailMapper.updateById(d);
|
|
|
+ }
|
|
|
+ String remark = sec.getRemark() != null ? sec.getRemark() : "";
|
|
|
+ sec.setOrderStatus(2);
|
|
|
+ sec.setRemark((remark + " [换桌合并至" + primary.getOrderNo() + "]").trim());
|
|
|
+ sec.setUpdatedUserId(userId);
|
|
|
+ sec.setUpdatedTime(now);
|
|
|
+ this.updateById(sec);
|
|
|
+ }
|
|
|
+ primary.setTotalAmount(total.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ primary.setPayAmount(pay.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ if (primary.getUserReservationId() == null) {
|
|
|
+ for (StoreOrder sec : secondaries) {
|
|
|
+ if (sec.getUserReservationId() != null) {
|
|
|
+ primary.setUserReservationId(sec.getUserReservationId());
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ primary.setUpdatedUserId(userId);
|
|
|
+ primary.setUpdatedTime(now);
|
|
|
+ this.updateById(primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void patchOrderTable(StoreOrder o, StoreTable newTable, Integer userId, Date now) {
|
|
|
+ o.setTableId(newTable.getId());
|
|
|
+ o.setTableNumber(newTable.getTableNumber());
|
|
|
+ o.setUpdatedUserId(userId);
|
|
|
+ o.setUpdatedTime(now);
|
|
|
+ this.updateById(o);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void assignTableToOrder(StoreTable table, StoreOrder order, Integer dinerCount) {
|
|
|
+ StoreTable patch = new StoreTable();
|
|
|
+ patch.setId(table.getId());
|
|
|
+ patch.setCurrentOrderId(order.getId());
|
|
|
+ patch.setStatus(1);
|
|
|
+ if (dinerCount != null) {
|
|
|
+ patch.setDinerCount(dinerCount);
|
|
|
+ }
|
|
|
+ storeTableMapper.updateById(patch);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 释放源桌:仅当 current_order_id 仍指向本订单时清空,避免误清其它会话。
|
|
|
+ */
|
|
|
+ private void releaseTableIfCurrentOrder(StoreTable table, Integer orderId) {
|
|
|
+ if (table == null || orderId == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (table.getCurrentOrderId() != null && table.getCurrentOrderId().equals(orderId)) {
|
|
|
+ StoreTable patch = new StoreTable();
|
|
|
+ patch.setId(table.getId());
|
|
|
+ patch.setCurrentOrderId(null);
|
|
|
+ patch.setStatus(0);
|
|
|
+ patch.setDinerCount(null);
|
|
|
+ storeTableMapper.updateById(patch);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<Integer, StoreTable> loadAndAssertStoreTables(Integer storeId, List<Integer> ids, String label) {
|
|
|
+ Map<Integer, StoreTable> map = new LinkedHashMap<>();
|
|
|
+ for (Integer id : ids) {
|
|
|
+ StoreTable t = storeTableMapper.selectById(id);
|
|
|
+ if (t == null || t.getStoreId() == null || !t.getStoreId().equals(storeId)) {
|
|
|
+ throw new IllegalArgumentException(label + "不存在或不属于该门店,tableId=" + id);
|
|
|
+ }
|
|
|
+ map.put(id, t);
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void assertTargetTableIdle(StoreTable t) {
|
|
|
+ if (t.getStatus() != null && t.getStatus() != 0) {
|
|
|
+ throw new IllegalArgumentException("目标桌非空闲,无法换入,tableId=" + t.getId());
|
|
|
+ }
|
|
|
+ if (t.getCurrentOrderId() != null) {
|
|
|
+ throw new IllegalArgumentException("目标桌已绑定订单,无法换入,tableId=" + t.getId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private StoreOrder findSinglePendingOrderForTable(Integer storeId, Integer tableId) {
|
|
|
+ LambdaQueryWrapper<StoreOrder> q = new LambdaQueryWrapper<>();
|
|
|
+ q.eq(StoreOrder::getStoreId, storeId)
|
|
|
+ .eq(StoreOrder::getTableId, tableId)
|
|
|
+ .eq(StoreOrder::getDeleteFlag, 0)
|
|
|
+ .eq(StoreOrder::getOrderStatus, 0)
|
|
|
+ .eq(StoreOrder::getPayStatus, 0)
|
|
|
+ .orderByDesc(StoreOrder::getId);
|
|
|
+ List<StoreOrder> list = this.list(q);
|
|
|
+ if (list.size() > 1) {
|
|
|
+ throw new IllegalStateException("该桌存在多笔待支付订单,数据异常,tableId=" + tableId);
|
|
|
+ }
|
|
|
+ return list.isEmpty() ? null : list.get(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验预约一致性:要么均无预约,要么同一 user_reservation_id;并按 carryReservationTime 决定是否改约。
|
|
|
+ */
|
|
|
+ private void validateReservationConsistency(List<StoreOrder> orders, StoreBookingChangeTableDTO dto) {
|
|
|
+ boolean hasNull = orders.stream().anyMatch(o -> o.getUserReservationId() == null);
|
|
|
+ boolean hasNonNull = orders.stream().anyMatch(o -> o.getUserReservationId() != null);
|
|
|
+ if (hasNull && hasNonNull) {
|
|
|
+ throw new IllegalArgumentException("所选订单部分含预约、部分为散客,无法一并换桌");
|
|
|
+ }
|
|
|
+ Set<Integer> resIds = orders.stream()
|
|
|
+ .map(StoreOrder::getUserReservationId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ if (resIds.size() > 1) {
|
|
|
+ throw new IllegalArgumentException("所选订单关联多笔不同预约,请分次换桌或联系管理员");
|
|
|
+ }
|
|
|
+ if (resIds.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Integer reservationId = resIds.iterator().next();
|
|
|
+ UserReservation ur = userReservationMapper.selectById(reservationId);
|
|
|
+ if (ur == null || (ur.getDeleteFlag() != null && ur.getDeleteFlag() != 0)) {
|
|
|
+ throw new IllegalArgumentException("预约不存在或已删除");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Boolean.TRUE.equals(dto.getCarryReservationTime())) {
|
|
|
+ // 多桌合并:若各订单曾对应不同预约已排除;此处仅校验「沿用」时不再写新字段
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (dto.getNewReservationDate() == null
|
|
|
+ || !StringUtils.hasText(dto.getNewStartTime())
|
|
|
+ || !StringUtils.hasText(dto.getNewEndTime())) {
|
|
|
+ throw new IllegalArgumentException("不沿用原预约时间时,请填写新预约日期与开始/结束时间");
|
|
|
+ }
|
|
|
+ UserReservation patch = new UserReservation();
|
|
|
+ patch.setId(reservationId);
|
|
|
+ patch.setReservationDate(dto.getNewReservationDate());
|
|
|
+ patch.setStartTime(dto.getNewStartTime().trim());
|
|
|
+ patch.setEndTime(dto.getNewEndTime().trim());
|
|
|
+ userReservationMapper.updateById(patch);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Integer unifiedReservationId(List<StoreOrder> orders) {
|
|
|
+ for (StoreOrder o : orders) {
|
|
|
+ if (o.getUserReservationId() != null) {
|
|
|
+ return o.getUserReservationId();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 与 {@link shop.alien.store.service.impl.UserReservationServiceImpl#saveReservationTables} 一致:先物理删再插,保证预约与桌台一致。
|
|
|
+ */
|
|
|
+ private void replaceReservationTables(Integer reservationId, List<Integer> newTableIds) {
|
|
|
+ userReservationTableMapper.physicalDeleteByReservationId(reservationId);
|
|
|
+ if (newTableIds == null || newTableIds.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ int sort = 0;
|
|
|
+ for (Integer tableId : newTableIds) {
|
|
|
+ UserReservationTable row = new UserReservationTable();
|
|
|
+ row.setReservationId(reservationId);
|
|
|
+ row.setTableId(tableId);
|
|
|
+ row.setSort(sort++);
|
|
|
+ userReservationTableMapper.insert(row);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析本桌待支付订单,用于首单/多次加餐。
|
|
|
+ * 优先级:显式 targetOrderId → 桌台 current_order_id → 按桌+店查询待支付(唯一一笔)。
|
|
|
+ */
|
|
|
+ private StoreOrder resolvePendingOrder(StoreBookingPlaceOrderDTO dto, StoreTable table) {
|
|
|
+ if (dto.getTargetOrderId() != null) {
|
|
|
+ StoreOrder o = this.getById(dto.getTargetOrderId());
|
|
|
+ if (!isPendingOrderForTable(o, dto.getStoreId(), dto.getTableId())) {
|
|
|
+ throw new IllegalArgumentException("目标订单不存在、非待支付或与门店/桌号不符,无法加餐");
|
|
|
+ }
|
|
|
+ return o;
|
|
|
+ }
|
|
|
+ Integer curId = table.getCurrentOrderId();
|
|
|
+ if (curId != null) {
|
|
|
+ StoreOrder byPointer = this.getById(curId);
|
|
|
+ if (isPendingOrderForTable(byPointer, dto.getStoreId(), dto.getTableId())) {
|
|
|
+ return byPointer;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ LambdaQueryWrapper<StoreOrder> q = new LambdaQueryWrapper<>();
|
|
|
+ q.eq(StoreOrder::getStoreId, dto.getStoreId())
|
|
|
+ .eq(StoreOrder::getTableId, dto.getTableId())
|
|
|
+ .eq(StoreOrder::getDeleteFlag, 0)
|
|
|
+ .eq(StoreOrder::getOrderStatus, 0)
|
|
|
+ .eq(StoreOrder::getPayStatus, 0)
|
|
|
+ .orderByDesc(StoreOrder::getId);
|
|
|
+ List<StoreOrder> list = this.list(q);
|
|
|
+ if (list.size() > 1) {
|
|
|
+ throw new IllegalStateException("该桌存在多笔待支付订单,数据异常,请联系管理员");
|
|
|
+ }
|
|
|
+ return list.isEmpty() ? null : list.get(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isPendingOrderForTable(StoreOrder o, Integer storeId, Integer tableId) {
|
|
|
+ if (o == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (o.getDeleteFlag() != null && o.getDeleteFlag() != 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (o.getOrderStatus() == null || o.getOrderStatus() != 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (o.getPayStatus() == null || o.getPayStatus() != 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return storeId.equals(o.getStoreId()) && tableId.equals(o.getTableId());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 加餐:在已有待支付订单上追加明细,累加金额;桌台置为加餐(3),与堂食约定一致。
|
|
|
+ */
|
|
|
+ private StoreBookingPlaceOrderResultVo addDishesToExistingOrder(
|
|
|
+ StoreBookingPlaceOrderDTO dto,
|
|
|
+ StoreTable table,
|
|
|
+ StoreOrder existing,
|
|
|
+ BigDecimal deltaAmount,
|
|
|
+ Integer userId,
|
|
|
+ String userPhone,
|
|
|
+ Date now) {
|
|
|
+ if (!dto.getStoreId().equals(existing.getStoreId()) || !dto.getTableId().equals(existing.getTableId())) {
|
|
|
+ throw new IllegalArgumentException("待支付订单与当前门店/桌号不一致,无法加餐");
|
|
|
+ }
|
|
|
+ BigDecimal oldTotal = existing.getTotalAmount() != null ? existing.getTotalAmount() : BigDecimal.ZERO;
|
|
|
+ BigDecimal oldPay = existing.getPayAmount() != null ? existing.getPayAmount() : BigDecimal.ZERO;
|
|
|
+ BigDecimal newTotal = oldTotal.add(deltaAmount).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ BigDecimal newPay = oldPay.add(deltaAmount).setScale(2, RoundingMode.HALF_UP);
|
|
|
+
|
|
|
+ existing.setTotalAmount(newTotal);
|
|
|
+ existing.setPayAmount(newPay);
|
|
|
+ existing.setDinerCount(dto.getDinerCount());
|
|
|
+ if (StringUtils.hasText(dto.getContactPhone())) {
|
|
|
+ existing.setContactPhone(dto.getContactPhone().trim());
|
|
|
+ }
|
|
|
+ if (StringUtils.hasText(dto.getDiningContactName())) {
|
|
|
+ existing.setDiningContactName(dto.getDiningContactName().trim());
|
|
|
+ }
|
|
|
+ if (dto.getDiningGender() != null) {
|
|
|
+ existing.setDiningGender(dto.getDiningGender());
|
|
|
+ }
|
|
|
+ if (StringUtils.hasText(dto.getRemark())) {
|
|
|
+ existing.setRemark(dto.getRemark().trim());
|
|
|
+ }
|
|
|
+ if (existing.getUserReservationId() == null && dto.getUserReservationId() != null) {
|
|
|
+ existing.setUserReservationId(dto.getUserReservationId());
|
|
|
+ }
|
|
|
+ existing.setUpdatedUserId(userId);
|
|
|
+ existing.setUpdatedTime(now);
|
|
|
+ this.updateById(existing);
|
|
|
+
|
|
|
+ insertDetailRows(existing.getId(), existing.getOrderNo(), dto.getItems(), userId, userPhone, now, true);
|
|
|
+
|
|
|
+ StoreTable tablePatch = new StoreTable();
|
|
|
+ tablePatch.setId(table.getId());
|
|
|
+ tablePatch.setCurrentOrderId(existing.getId());
|
|
|
+ tablePatch.setStatus(3);
|
|
|
+ tablePatch.setDinerCount(dto.getDinerCount());
|
|
|
+ storeTableMapper.updateById(tablePatch);
|
|
|
+
|
|
|
+ StoreBookingPlaceOrderResultVo vo = new StoreBookingPlaceOrderResultVo();
|
|
|
+ vo.setOrderId(existing.getId());
|
|
|
+ vo.setOrderNo(existing.getOrderNo());
|
|
|
+ vo.setTotalAmount(newTotal);
|
|
|
+ vo.setAddDish(true);
|
|
|
+ log.info("预订加餐成功 orderId={} orderNo={} delta={} tableId={}", existing.getId(), existing.getOrderNo(), deltaAmount, dto.getTableId());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 校验菜品归属并汇总本请求明细金额 */
|
|
|
+ private BigDecimal validateAndSumLines(StoreBookingPlaceOrderDTO dto) {
|
|
|
+ BigDecimal sum = BigDecimal.ZERO;
|
|
|
+ for (StoreBookingPlaceOrderItemDTO line : dto.getItems()) {
|
|
|
+ StoreCuisine cuisine = storeCuisineMapper.selectById(line.getCuisineId());
|
|
|
+ if (cuisine == null || cuisine.getStoreId() == null || !cuisine.getStoreId().equals(dto.getStoreId())) {
|
|
|
+ throw new IllegalArgumentException("菜品不存在或不属于该门店,cuisineId=" + line.getCuisineId());
|
|
|
+ }
|
|
|
+ BigDecimal unit = line.getUnitPrice() != null ? line.getUnitPrice() : BigDecimal.ZERO;
|
|
|
+ BigDecimal sub = unit.multiply(BigDecimal.valueOf(line.getQuantity())).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ sum = sum.add(sub);
|
|
|
+ }
|
|
|
+ return sum;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void insertDetailRows(
|
|
|
+ Integer orderId,
|
|
|
+ String orderNo,
|
|
|
+ List<StoreBookingPlaceOrderItemDTO> lines,
|
|
|
+ Integer userId,
|
|
|
+ String userPhone,
|
|
|
+ Date now,
|
|
|
+ boolean addDish) {
|
|
|
+ for (StoreBookingPlaceOrderItemDTO line : lines) {
|
|
|
+ BigDecimal unit = line.getUnitPrice() != null ? line.getUnitPrice() : BigDecimal.ZERO;
|
|
|
+ int qty = line.getQuantity() != null ? line.getQuantity() : 1;
|
|
|
+ BigDecimal sub = unit.multiply(BigDecimal.valueOf(qty)).setScale(2, RoundingMode.HALF_UP);
|
|
|
+ Integer cuisineType = line.getCuisineType() != null ? line.getCuisineType() : 1;
|
|
|
+
|
|
|
+ StoreOrderDetail detail = new StoreOrderDetail();
|
|
|
+ detail.setOrderId(orderId);
|
|
|
+ detail.setOrderNo(orderNo);
|
|
|
+ detail.setCuisineId(line.getCuisineId());
|
|
|
+ detail.setCuisineName(line.getCuisineName());
|
|
|
+ detail.setCuisineType(cuisineType);
|
|
|
+ detail.setCuisineImage(line.getCuisineImage());
|
|
|
+ detail.setUnitPrice(unit);
|
|
|
+ detail.setQuantity(qty);
|
|
|
+ detail.setSubtotalAmount(sub);
|
|
|
+ detail.setAddUserId(userId);
|
|
|
+ detail.setAddUserPhone(userPhone);
|
|
|
+ detail.setIsAddDish(addDish ? 1 : 0);
|
|
|
+ if (addDish) {
|
|
|
+ detail.setAddDishTime(now);
|
|
|
+ }
|
|
|
+ detail.setCreatedUserId(userId);
|
|
|
+ detail.setUpdatedUserId(userId);
|
|
|
+ detail.setCreatedTime(now);
|
|
|
+ detail.setUpdatedTime(now);
|
|
|
+ storeOrderDetailMapper.insert(detail);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 与 alien-dining 订单号规则保持一致 */
|
|
|
+ private static String generateOrderNo() {
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
|
|
|
+ String timestamp = sdf.format(new Date());
|
|
|
+ String random = String.valueOf((int) (Math.random() * 10000));
|
|
|
+ return "ORD" + timestamp + String.format("%04d", Integer.parseInt(random));
|
|
|
+ }
|
|
|
+}
|