|
|
@@ -13,6 +13,7 @@ import org.springframework.util.StringUtils;
|
|
|
import shop.alien.dining.config.BaseRedisService;
|
|
|
import shop.alien.dining.service.CartService;
|
|
|
import shop.alien.dining.service.StoreOrderService;
|
|
|
+import shop.alien.dining.support.DiningMenuPricing;
|
|
|
import shop.alien.dining.util.TokenUtil;
|
|
|
import shop.alien.entity.store.*;
|
|
|
import shop.alien.entity.store.dto.CartDTO;
|
|
|
@@ -29,7 +30,10 @@ import shop.alien.entity.store.vo.OrderDetailWithChangeLogVO;
|
|
|
import shop.alien.mapper.*;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.ZoneId;
|
|
|
import java.util.*;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
@@ -45,6 +49,9 @@ import java.util.stream.Collectors;
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOrder> implements StoreOrderService {
|
|
|
|
|
|
+ private static final ZoneId SHANGHAI = ZoneId.of("Asia/Shanghai");
|
|
|
+ /** user_reservation.status:已到店,仅该状态允许下单绑预约 */
|
|
|
+ private static final int RESERVATION_STATUS_ARRIVED = 2;
|
|
|
|
|
|
private final StoreOrderDetailMapper orderDetailMapper;
|
|
|
private final StoreTableMapper storeTableMapper;
|
|
|
@@ -62,6 +69,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
private final shop.alien.dining.service.OrderLockService orderLockService;
|
|
|
private final UserReservationMapper userReservationMapper;
|
|
|
private final UserReservationTableMapper userReservationTableMapper;
|
|
|
+ private final StoreProductDiscountRuleMapper storeProductDiscountRuleMapper;
|
|
|
|
|
|
@Override
|
|
|
public StoreOrder createOrder(CreateOrderDTO dto) {
|
|
|
@@ -212,10 +220,18 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
order.setDiscountAmount(discountAmount);
|
|
|
order.setPayAmount(payAmount);
|
|
|
order.setRemark(dto.getRemark());
|
|
|
- if (dto.getUserReservationId() != null) {
|
|
|
- validateUserReservationForOrder(dto.getUserReservationId(), table);
|
|
|
- order.setUserReservationId(dto.getUserReservationId());
|
|
|
+ Integer bindReservationId = dto.getUserReservationId();
|
|
|
+ if (bindReservationId == null) {
|
|
|
+ bindReservationId = order.getUserReservationId();
|
|
|
}
|
|
|
+ if (bindReservationId == null) {
|
|
|
+ bindReservationId = resolveUserReservationIdForTable(table);
|
|
|
+ }
|
|
|
+ if (bindReservationId == null) {
|
|
|
+ throw new RuntimeException("未找到与本桌关联的有效已到店预约,无法更新订单");
|
|
|
+ }
|
|
|
+ validateUserReservationForOrder(bindReservationId, table);
|
|
|
+ order.setUserReservationId(bindReservationId);
|
|
|
order.setUpdatedUserId(userId);
|
|
|
order.setUpdatedTime(now);
|
|
|
this.updateById(order);
|
|
|
@@ -239,10 +255,14 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
|
|
|
// 如果没有订单需要更新,创建新订单
|
|
|
if (!isUpdate) {
|
|
|
- if (dto.getUserReservationId() == null) {
|
|
|
- throw new RuntimeException("请先完成就餐信息登记后再下单(需传入预约ID)");
|
|
|
+ Integer userReservationId = dto.getUserReservationId();
|
|
|
+ if (userReservationId == null) {
|
|
|
+ userReservationId = resolveUserReservationIdForTable(table);
|
|
|
+ }
|
|
|
+ if (userReservationId == null) {
|
|
|
+ throw new RuntimeException("请先完成就餐信息登记后再下单(未找到与本桌关联的有效预约)");
|
|
|
}
|
|
|
- validateUserReservationForOrder(dto.getUserReservationId(), table);
|
|
|
+ validateUserReservationForOrder(userReservationId, table);
|
|
|
// 生成订单号
|
|
|
orderNo = generateOrderNo();
|
|
|
|
|
|
@@ -269,7 +289,7 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
// payType在支付时设置
|
|
|
order.setPayStatus(0); // 未支付
|
|
|
order.setRemark(dto.getRemark());
|
|
|
- order.setUserReservationId(dto.getUserReservationId());
|
|
|
+ order.setUserReservationId(userReservationId);
|
|
|
order.setCreatedUserId(userId);
|
|
|
order.setUpdatedUserId(userId);
|
|
|
// 手动设置创建时间和更新时间(临时方案,避免自动填充未生效导致 created_time 为 null)
|
|
|
@@ -324,11 +344,23 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 创建订单明细(单价、小计来自购物车)
|
|
|
+ // 创建订单明细:标价/成交单价按当前时刻 store_cuisine + 菜品优惠规则计算(与购物车刷新口径一致)
|
|
|
// 如果是更新订单,需要判断哪些商品是新增的或数量增加的,标记为加餐
|
|
|
// 餐具的特殊ID(用于标识餐具项)
|
|
|
final Integer TABLEWARE_CUISINE_ID = -1;
|
|
|
-
|
|
|
+
|
|
|
+ java.util.Set<Integer> cartCuisineIds = cart.getItems().stream()
|
|
|
+ .map(CartItemDTO::getCuisineId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .filter(id -> id > 0)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ java.util.Map<Integer, java.math.BigDecimal> listUnitByCuisine = cartCuisineIds.isEmpty()
|
|
|
+ ? java.util.Collections.emptyMap()
|
|
|
+ : DiningMenuPricing.resolveListUnitPriceByCuisineId(cartCuisineIds, storeCuisineMapper);
|
|
|
+ java.util.Map<Integer, java.math.BigDecimal> saleUnitByCuisine = cartCuisineIds.isEmpty()
|
|
|
+ ? java.util.Collections.emptyMap()
|
|
|
+ : DiningMenuPricing.resolveSaleUnitPrice(table.getStoreId(), listUnitByCuisine, storeProductDiscountRuleMapper);
|
|
|
+
|
|
|
List<StoreOrderDetail> orderDetails = cart.getItems().stream()
|
|
|
.map(item -> {
|
|
|
StoreOrderDetail detail = new StoreOrderDetail();
|
|
|
@@ -361,9 +393,25 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
detail.setCuisineType(cuisineType);
|
|
|
|
|
|
detail.setCuisineImage(item.getCuisineImage());
|
|
|
- detail.setUnitPrice(item.getUnitPrice());
|
|
|
+ int lineQty = item.getQuantity() != null ? item.getQuantity() : 0;
|
|
|
+ if (TABLEWARE_CUISINE_ID.equals(item.getCuisineId())) {
|
|
|
+ java.math.BigDecimal p = item.getUnitPrice() != null ? item.getUnitPrice() : BigDecimal.ZERO;
|
|
|
+ detail.setListUnitPrice(p);
|
|
|
+ detail.setUnitPrice(p);
|
|
|
+ detail.setSubtotalAmount(p.multiply(BigDecimal.valueOf(lineQty)).setScale(2, RoundingMode.HALF_UP));
|
|
|
+ } else {
|
|
|
+ Integer cid = item.getCuisineId();
|
|
|
+ java.math.BigDecimal listU = listUnitByCuisine.getOrDefault(cid,
|
|
|
+ item.getOriginalUnitPrice() != null ? item.getOriginalUnitPrice()
|
|
|
+ : (item.getUnitPrice() != null ? item.getUnitPrice() : BigDecimal.ZERO));
|
|
|
+ java.math.BigDecimal saleU = saleUnitByCuisine.getOrDefault(cid,
|
|
|
+ item.getCurrentUnitPrice() != null ? item.getCurrentUnitPrice()
|
|
|
+ : (item.getUnitPrice() != null ? item.getUnitPrice() : listU));
|
|
|
+ detail.setListUnitPrice(listU);
|
|
|
+ detail.setUnitPrice(saleU);
|
|
|
+ detail.setSubtotalAmount(saleU.multiply(BigDecimal.valueOf(lineQty)).setScale(2, RoundingMode.HALF_UP));
|
|
|
+ }
|
|
|
detail.setQuantity(item.getQuantity());
|
|
|
- detail.setSubtotalAmount(item.getSubtotalAmount());
|
|
|
detail.setAddUserId(item.getAddUserId());
|
|
|
detail.setAddUserPhone(item.getAddUserPhone());
|
|
|
detail.setRemark(item.getRemark());
|
|
|
@@ -809,7 +857,11 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
item.setCuisineName(detail.getCuisineName());
|
|
|
item.setCuisineImage(detail.getCuisineImage());
|
|
|
item.setQuantity(detail.getQuantity());
|
|
|
- item.setUnitPrice(detail.getUnitPrice());
|
|
|
+ BigDecimal cur = detail.getUnitPrice();
|
|
|
+ BigDecimal orig = detail.getListUnitPrice() != null ? detail.getListUnitPrice() : cur;
|
|
|
+ item.setUnitPrice(cur);
|
|
|
+ item.setOriginalUnitPrice(orig);
|
|
|
+ item.setCurrentUnitPrice(cur);
|
|
|
item.setTags(detail.getCuisineId() != null ? finalCuisineIdToTags.get(detail.getCuisineId()) : null);
|
|
|
return item;
|
|
|
})
|
|
|
@@ -1369,7 +1421,11 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
item.setCuisineName(detail.getCuisineName());
|
|
|
item.setCuisineImage(detail.getCuisineImage());
|
|
|
item.setQuantity(detail.getQuantity());
|
|
|
- item.setUnitPrice(detail.getUnitPrice());
|
|
|
+ BigDecimal cur = detail.getUnitPrice();
|
|
|
+ BigDecimal orig = detail.getListUnitPrice() != null ? detail.getListUnitPrice() : cur;
|
|
|
+ item.setUnitPrice(cur);
|
|
|
+ item.setOriginalUnitPrice(orig);
|
|
|
+ item.setCurrentUnitPrice(cur);
|
|
|
item.setTags(detail.getCuisineId() != null ? finalCuisineIdToTagsMy.get(detail.getCuisineId()) : null);
|
|
|
return item;
|
|
|
})
|
|
|
@@ -1428,7 +1484,12 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
item.setCuisineName(detail.getCuisineName());
|
|
|
item.setCuisineType(detail.getCuisineType());
|
|
|
item.setCuisineImage(detail.getCuisineImage());
|
|
|
- item.setUnitPrice(detail.getUnitPrice());
|
|
|
+ BigDecimal cur = detail.getUnitPrice();
|
|
|
+ BigDecimal orig = detail.getListUnitPrice() != null ? detail.getListUnitPrice() : cur;
|
|
|
+ item.setUnitPrice(cur);
|
|
|
+ item.setOriginalUnitPrice(orig);
|
|
|
+ item.setCurrentUnitPrice(cur);
|
|
|
+ item.setHasActiveDiscount(orig != null && cur != null && orig.compareTo(cur) != 0);
|
|
|
item.setQuantity(detail.getQuantity());
|
|
|
item.setSubtotalAmount(detail.getSubtotalAmount());
|
|
|
item.setAddUserId(detail.getAddUserId());
|
|
|
@@ -1782,7 +1843,59 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 校验预约存在、归属门店一致,且当前桌在该预约占用桌位中;不限制预约状态(延迟任务仅在已到店时更新)。
|
|
|
+ * 根据桌号解析可绑定的预约:user_reservation_table 关联本桌、门店一致、
|
|
|
+ * 状态为已到店(2)、预约日为今日(上海时区)。
|
|
|
+ * 不要求下单用户与预约登记用户一致,便于同伴在同一桌扫码下单。
|
|
|
+ * 若当日该桌有多条已到店预约,取预约 id 最大的一条。
|
|
|
+ */
|
|
|
+ private Integer resolveUserReservationIdForTable(StoreTable table) {
|
|
|
+ if (table == null || table.getId() == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ List<UserReservationTable> links = userReservationTableMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<UserReservationTable>()
|
|
|
+ .eq(UserReservationTable::getTableId, table.getId())
|
|
|
+ .eq(UserReservationTable::getDeleteFlag, 0));
|
|
|
+ if (links == null || links.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Set<Integer> resIds = links.stream()
|
|
|
+ .map(UserReservationTable::getReservationId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ if (resIds.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ List<UserReservation> candidates = userReservationMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<UserReservation>()
|
|
|
+ .in(UserReservation::getId, resIds)
|
|
|
+ .eq(UserReservation::getStoreId, table.getStoreId()));
|
|
|
+ if (candidates == null || candidates.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ LocalDate today = LocalDate.now(SHANGHAI);
|
|
|
+ List<UserReservation> active = candidates.stream()
|
|
|
+ .filter(r -> r.getStatus() != null && r.getStatus() == RESERVATION_STATUS_ARRIVED)
|
|
|
+ .filter(r -> reservationDateMatchesToday(r, today))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ if (active.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ active.sort(Comparator.comparing(UserReservation::getId, Comparator.nullsFirst(Comparator.naturalOrder())).reversed());
|
|
|
+ return active.get(0).getId();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean reservationDateMatchesToday(UserReservation r, LocalDate today) {
|
|
|
+ if (r.getReservationDate() == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ LocalDate d = r.getReservationDate().toInstant().atZone(SHANGHAI).toLocalDate();
|
|
|
+ return today.equals(d);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验预约存在、归属门店一致、当前桌在该预约占用桌位中、已到店(2)。
|
|
|
+ * 下单账号可与预约登记用户不同(同伴同桌下单)。
|
|
|
*/
|
|
|
private void validateUserReservationForOrder(Integer reservationId, StoreTable table) {
|
|
|
if (reservationId == null || table == null) {
|
|
|
@@ -1792,6 +1905,9 @@ public class StoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper, StoreOr
|
|
|
if (r == null) {
|
|
|
throw new RuntimeException("预约不存在");
|
|
|
}
|
|
|
+ if (r.getStatus() == null || r.getStatus() != RESERVATION_STATUS_ARRIVED) {
|
|
|
+ throw new RuntimeException("仅预约状态为已到店时可下单,请先完成到店登记");
|
|
|
+ }
|
|
|
if (r.getStoreId() == null || !r.getStoreId().equals(table.getStoreId())) {
|
|
|
throw new RuntimeException("预约与门店不匹配");
|
|
|
}
|