刘云鑫 před 2 týdny
rodič
revize
bca122501c

+ 8 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreOrder.java

@@ -62,6 +62,14 @@ public class StoreOrder {
     @TableField("contact_phone")
     private String contactPhone;
 
+    @ApiModelProperty(value = "下单联系人姓名")
+    @TableField("dining_contact_name")
+    private String diningContactName;
+
+    @ApiModelProperty(value = "联系人性别(1男 2女)")
+    @TableField("dining_gender")
+    private Integer diningGender;
+
     @ApiModelProperty(value = "餐具费")
     @TableField("tableware_fee")
     private BigDecimal tablewareFee;

+ 62 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingPlaceOrderDTO.java

@@ -0,0 +1,62 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.List;
+
+/**
+ * 预订场景「下单」页提交:主表写入 store_order,明细写入 store_order_detail。
+ * <p>
+ * 同一桌已存在待支付订单时,接口行为为「加餐」:只追加本次提交的明细(is_add_dish=1),并累加订单金额,不生成新订单号。
+ * 同一笔待支付订单可多次调用本接口追加(多次加餐),不限次数。
+ */
+@Data
+@ApiModel(value = "StoreBookingPlaceOrderDTO", description = "预订下单请求")
+public class StoreBookingPlaceOrderDTO {
+
+    @ApiModelProperty(value = "门店ID", required = true)
+    @NotNull(message = "门店ID不能为空")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "桌号ID", required = true)
+    @NotNull(message = "桌号ID不能为空")
+    private Integer tableId;
+
+    @ApiModelProperty(value = "加餐目标订单ID(可选)。若桌台 current_order_id 未维护或需显式指定,可传上次返回的 orderId,避免误开新单")
+    private Integer targetOrderId;
+
+    @ApiModelProperty(value = "关联用户预约ID(user_reservation.id,无则不传)")
+    private Integer userReservationId;
+
+    @ApiModelProperty(value = "使用人数(就餐人数)", required = true)
+    @NotNull(message = "使用人数不能为空")
+    @Min(value = 1, message = "使用人数至少为1")
+    private Integer dinerCount;
+
+    @ApiModelProperty(value = "下单联系人姓名,对应 store_order.dining_contact_name")
+    @Size(max = 100, message = "联系人姓名过长")
+    private String diningContactName;
+
+    @ApiModelProperty(value = "手机号")
+    @Size(max = 20, message = "手机号过长")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "联系人性别(1男 2女),对应 store_order.dining_gender")
+    private Integer diningGender;
+
+    @ApiModelProperty(value = "备注,最多30字")
+    @Size(max = 30, message = "备注最多30字")
+    private String remark;
+
+    @ApiModelProperty(value = "菜品明细", required = true)
+    @NotEmpty(message = "请至少选择一道菜品")
+    @Valid
+    private List<StoreBookingPlaceOrderItemDTO> items;
+}

+ 41 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreBookingPlaceOrderItemDTO.java

@@ -0,0 +1,41 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+/**
+ * 预订场景下单:订单明细行(写入 store_order_detail)
+ */
+@Data
+@ApiModel(value = "StoreBookingPlaceOrderItemDTO", description = "预订下单-菜品行")
+public class StoreBookingPlaceOrderItemDTO {
+
+    @ApiModelProperty(value = "菜品ID", required = true)
+    @NotNull(message = "菜品ID不能为空")
+    private Integer cuisineId;
+
+    @ApiModelProperty(value = "菜品名称", required = true)
+    @NotBlank(message = "菜品名称不能为空")
+    private String cuisineName;
+
+    @ApiModelProperty(value = "菜品类型(1:单品, 2:套餐),不传默认1")
+    private Integer cuisineType;
+
+    @ApiModelProperty(value = "菜品图片")
+    private String cuisineImage;
+
+    @ApiModelProperty(value = "单价", required = true)
+    @NotNull(message = "单价不能为空")
+    private BigDecimal unitPrice;
+
+    @ApiModelProperty(value = "数量", required = true)
+    @NotNull(message = "数量不能为空")
+    @Min(value = 1, message = "数量至少为1")
+    private Integer quantity;
+}

+ 27 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreBookingPlaceOrderResultVo.java

@@ -0,0 +1,27 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 预订下单成功返回
+ */
+@Data
+@ApiModel(value = "StoreBookingPlaceOrderResultVo", description = "预订下单结果")
+public class StoreBookingPlaceOrderResultVo {
+
+    @ApiModelProperty(value = "订单ID")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "订单号")
+    private String orderNo;
+
+    @ApiModelProperty(value = "订单总金额(后端按明细汇总)")
+    private BigDecimal totalAmount;
+
+    @ApiModelProperty(value = "本次是否为加餐追加(true:追加明细;false:新订单首单)")
+    private Boolean addDish;
+}

+ 45 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreBookingOrderController.java

@@ -0,0 +1,45 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.StoreBookingPlaceOrderDTO;
+import shop.alien.entity.store.vo.StoreBookingPlaceOrderResultVo;
+import shop.alien.store.service.StoreBookingOrderService;
+
+import javax.validation.Valid;
+
+/**
+ * 预订服务-门店订单(store_order / store_order_detail,不经过 alien-dining)
+ */
+@Slf4j
+@Api(tags = {"预订服务-门店下单"})
+@CrossOrigin
+@RestController
+@RequestMapping("/store/booking/order")
+@RequiredArgsConstructor
+public class StoreBookingOrderController {
+
+    private final StoreBookingOrderService storeBookingOrderService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation("提交订单(主表 store_order,明细 store_order_detail;联系人 dining_contact_name / dining_gender 1男2女。若该桌已有待支付订单则为加餐,可多次加餐;可选 targetOrderId 指定订单)")
+    @PostMapping("/place")
+    public R<StoreBookingPlaceOrderResultVo> placeOrder(@Valid @RequestBody StoreBookingPlaceOrderDTO dto) {
+        log.info("StoreBookingOrderController.placeOrder?storeId={}, tableId={}, dinerCount={}, itemSize={}",
+                dto.getStoreId(), dto.getTableId(), dto.getDinerCount(), dto.getItems() != null ? dto.getItems().size() : 0);
+        try {
+            return R.data(storeBookingOrderService.placeOrder(dto));
+        } catch (IllegalArgumentException | IllegalStateException e) {
+            log.warn("placeOrder biz: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("placeOrder error", e);
+            return R.fail("下单失败:" + e.getMessage());
+        }
+    }
+}

+ 15 - 0
alien-store/src/main/java/shop/alien/store/service/StoreBookingOrderService.java

@@ -0,0 +1,15 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.dto.StoreBookingPlaceOrderDTO;
+import shop.alien.entity.store.vo.StoreBookingPlaceOrderResultVo;
+
+/**
+ * 预订场景门店订单(仅 alien-store:落库 store_order / store_order_detail)
+ */
+public interface StoreBookingOrderService {
+
+    /**
+     * 提交下单:写主表、明细,并更新桌台当前订单(与堂食订单表结构一致)
+     */
+    StoreBookingPlaceOrderResultVo placeOrder(StoreBookingPlaceOrderDTO dto);
+}

+ 292 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreBookingOrderServiceImpl.java

@@ -0,0 +1,292 @@
+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.dto.StoreBookingPlaceOrderDTO;
+import shop.alien.entity.store.dto.StoreBookingPlaceOrderItemDTO;
+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.store.service.StoreBookingOrderService;
+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.Date;
+import java.util.List;
+
+/**
+ * 预订下单:不依赖 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;
+
+    @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;
+    }
+
+    /**
+     * 解析本桌待支付订单,用于首单/多次加餐。
+     * 优先级:显式 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));
+    }
+}