苍穹外卖【day09| 对订单的各种操作】

🌈个人主页 :一条泥憨鱼(欢迎各位大佬莅临)

🎬精选专栏传送门:

❄️《数据结构》 ❄️《AI与Agent那些事》

❄️《从0开始学计算机网络》 ❄️《后端开发》

订单搜索 GET /admin/order/conditionSearch

Controller 层 (OrderController)

java 复制代码
@GetMapping("/conditionSearch")
public Result<PageResult> conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
    PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO);
    return Result.success(pageResult);
}
  • 入参 OrdersPageQueryDTO 是查询条件:订单号、手机号、状态、下单时间段、用户ID、分页参数
  • 这是 GET 请求,参数通过 URL query string 传递,不是 request body

Service 层 (OrderServiceImpl)

java 复制代码
public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
    // ① PageHelper 开启分页 --- 它会自动给 SQL 加 LIMIT
    PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());

    // ② Mapper 动态条件查询(XML 里写的 <if> 标签)
    Page<Orders> page = (Page<Orders>) orderMapper.pageQuery(ordersPageQueryDTO);

    // ③ Orders 只存了基础信息,要拼成前端需要的 OrderVO(带菜品详情字符串)
    List<OrderVO> orderVOList = getOrderVOList(page);

    return new PageResult(page.getTotal(), orderVOList);
}

关键点在 getOrderVOList (OrderServiceImpl):

java 复制代码
private List<OrderVO> getOrderVOList(Page<Orders> page) {
    List<OrderVO> orderVOList = new ArrayList<>();
    List<Orders> ordersList = page.getResult();
    if (!CollectionUtils.isEmpty(ordersList)) {
        for (Orders orders : ordersList) {
            OrderVO orderVO = new OrderVO();
            BeanUtils.copyProperties(orders, orderVO);
            // 调下面那个方法拼菜品字符串
            String orderDishes = getOrderDishesStr(orders);
            orderVO.setOrderDishes(orderDishes); // 例如 "宫保鸡丁*2;米饭*1;"
            orderVOList.add(orderVO);
        }
    }
    return orderVOList;
}

getOrderDishesStr (OrderServiceImpl):

java 复制代码
private String getOrderDishesStr(Orders orders) {
    // 查 order_detail 表
    List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());
    // 拼字符串:每个菜品 "菜名*数量;"
    List<String> orderDishList = orderDetailList.stream().map(x -> {
        return x.getName() + "*" + x.getNumber() + ";";
    }).collect(Collectors.toList());
    return String.join("", orderDishList);
}

Mapper 层 (XML SQL)

XML 复制代码
<select id="pageQuery" resultType="com.sky.entity.Orders">
    select * from orders
    <where>
        <if test="number != null and number != ''">
            and number like concat('%', #{number}, '%')
        </if>
        <if test="phone != null and phone != ''">
            and phone like concat('%', #{phone}, '%')
        </if>
        <if test="status != null">
            and status = #{status}
        </if>
        <if test="beginTime != null">
            and order_time <![CDATA[>=]]> #{beginTime}
        </if>
        <if test="endTime != null">
            and order_time <![CDATA[<=]]> #{endTime}
        </if>
    </where>
    order by order_time desc
</select>

<![CDATA[>=]]> 是因为 MyBatis XML 里 > < 不能直接写,要用 CDATA 包裹。


各状态订单数量统计 GET /admin/order/statistics

Controller 层 (OrderController)

java 复制代码
@GetMapping("/statistics")
public Result<OrderStatisticsVO> statistics() {
    OrderStatisticsVO orderStatisticsVO = orderService.statistics();
    return Result.success(orderStatisticsVO);
}

Service 层 (OrderServiceImpl)

java 复制代码
public OrderStatisticsVO statistics() {
    // 三个状态对应的订单数量:待接单(2)、已接单(3)、派送中(4)
    Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED);       // status=2
    Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED);                 // status=3
    Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS); // status=4

    OrderStatisticsVO vo = new OrderStatisticsVO();
    vo.setToBeConfirmed(toBeConfirmed);
    vo.setConfirmed(confirmed);
    vo.setDeliveryInProgress(deliveryInProgress);
    return vo;
}

Mapper 层 (注解方式)

java 复制代码
@Select("select count(*) from orders where status = #{status}")
Integer countStatus(Integer status);

返回的 VO (OrderStatisticsVO):

java 复制代码
public class OrderStatisticsVO {
    private Integer toBeConfirmed;      // 待接单数量
    private Integer confirmed;          // 待派送数量
    private Integer deliveryInProgress; // 派送中数量
}

这个接口是给商家首页做三个 Tab Badge 用的,告诉你每个状态有多少单。


查询订单详情 GET /admin/order/details/{id}

Controller 层 (OrderController)

java 复制代码
@GetMapping("/details/{id}")
public Result<OrderVO> details(@PathVariable("id") Long id) {
    OrderVO orderVO = orderService.details(id);
    return Result.success(orderVO);
}

Service 层 (OrderServiceImpl)

java 复制代码
public OrderVO details(Long id) {
    // ① 查订单主表
    Orders orders = orderMapper.getById(id);

    // ② 查订单明细表(order_detail 表)
    List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

    // ③ 封装成 OrderVO(继承了 Orders,再加一个 orderDetailList 字段)
    OrderVO orderVO = new OrderVO();
    BeanUtils.copyProperties(orders, orderVO);
    orderVO.setOrderDetailList(orderDetailList);

    return orderVO;
}

Mapper 层 (都是注解)

java 复制代码
// OrderMapper
@Select("select * from orders where id = #{id}")
Orders getById(Long id);

// OrderDetailMapper
@Select("select * from order_detail where order_id = #{orderId}")
List<OrderDetail> getByOrderId(Long orderId);

OrderVO 结构:继承 Orders(有 number、status、amount...)+ orderDetailList(菜品明细列表)+ orderDishes(菜品字符串)。一个查询,两次数据库调用,最后合成一个 VO 返回。


接单 PUT /admin/order/confirm

Controller 层 (OrderController)

java 复制代码
@PutMapping("/confirm")
public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) {
    orderService.confirm(ordersConfirmDTO);
    return Result.success();
}

Service 层 (OrderServiceImpl)

java 复制代码
public void confirm(OrdersConfirmDTO ordersConfirmDTO) {
    Orders orders = Orders.builder()
            .id(ordersConfirmDTO.getId())   // 订单ID
            .status(Orders.CONFIRMED)       // 状态改成 3(已接单/待派送)
            .build();

    orderMapper.update(orders);
}

DTO (OrdersConfirmDTO):

java 复制代码
public class OrdersConfirmDTO {
    private Long id;
    private Integer status; // 这里虽然有 status 字段,但 Service 里没用它,而是硬编码了 CONFIRMED
}

这里业务逻辑很简单 --- 把订单状态从 2(待接单) 改成 3(已接单)。注意没有做状态前置校验(比如确认订单是不是真的处于「待接单」状态),而拒单、派送、完成这些操作都做了。这是个小瑕疵。


拒单 PUT /admin/order/rejection

Controller 层 (OrderController)

java 复制代码
@PutMapping("/rejection")
public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) {
    orderService.rejection(ordersRejectionDTO);
    return Result.success();
}

Service 层 (OrderServiceImpl) --- 这是最复杂的接口

java 复制代码
public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
    // ① 先查订单,确保存在
    Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId());

    // ② 前置校验:订单必须存在 且 状态是 2(待接单),才允许拒单
    if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
        throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
    }

    // ③ 如果用户已经付过钱了 → 调用微信退款接口
    Integer payStatus = ordersDB.getPayStatus();
    if (payStatus == Orders.PAID) {
        try {
            String refund = weChatPayUtil.refund(
                    ordersDB.getNumber(),    // 商户订单号
                    ordersDB.getNumber(),    // 商户退款单号
                    new BigDecimal(0.01),    // 退款金额(演示用的 0.01 元)
                    new BigDecimal(0.01));   // 原订单金额
            log.info("申请退款:{}", refund);
        } catch (Exception e) {
            // 本地环境没配微信商户证书,退款会失败 --- 但不让退款失败阻断拒单流程
            log.warn("微信退款接口调用失败,本地环境模拟退款:{}", e.getMessage());
        }
    }

    // ④ 更新订单:状态改成已取消 + 记录拒单原因和取消时间
    Orders orders = new Orders();
    orders.setId(ordersDB.getId());
    orders.setStatus(Orders.CANCELLED);            // 状态 6
    orders.setRejectionReason(ordersRejectionDTO.getRejectionReason()); // 商家填的拒单理由
    orders.setCancelTime(LocalDateTime.now());

    orderMapper.update(orders);
}

三步走:校验 → 退款 → 改状态。 注意退款用了 try-catch 兜底,因为本地开发环境没有真实微信商户证书,退款一定会失败,但我们不希望退款失败导致拒单也失败 ,所以 catch 里只记日志,不抛出。这是一个好的容错设计


取消订单 PUT /admin/order/cancel

Controller 层 (OrderController)

java 复制代码
@PutMapping("/cancel")
public Result cancel(@RequestBody OrdersCancelDTO ordersCancelDTO) {
    orderService.cancel(ordersCancelDTO);
    return Result.success();
}

Service 层 (OrderServiceImpl)

java 复制代码
public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception {
    Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId());

    // ① 如果已支付,也要退款
    Integer payStatus = ordersDB.getPayStatus();
    if (payStatus == 1) {
        try {
            weChatPayUtil.refund(
                    ordersDB.getNumber(),
                    ordersDB.getNumber(),
                    new BigDecimal(0.01),
                    new BigDecimal(0.01));
        } catch (Exception e) {
            log.warn("微信退款接口调用失败,本地环境模拟退款:{}", e.getMessage());
        }
    }

    // ② 更新状态为已取消 + 记录取消原因
    Orders orders = new Orders();
    orders.setId(ordersCancelDTO.getId());
    orders.setStatus(Orders.CANCELLED);
    orders.setCancelReason(ordersCancelDTO.getCancelReason()); // 商家填的取消理由
    orders.setCancelTime(LocalDateTime.now());
    orderMapper.update(orders);
}

和拒单逻辑几乎一模一样,区别只是没有做状态前置校验 (商家可以取消任意状态的订单?这在真实业务中需要限制)。拒单用的是 rejectionReason,取消用的是 cancelReason


派送订单 PUT /admin/order/delivery/{id}

Controller 层 (OrderController)

java 复制代码
@PutMapping("/delivery/{id}")
public Result delivery(@PathVariable("id") Long id) {
    orderService.delivery(id);
    return Result.success();
}

Service 层 (OrderServiceImpl)

java 复制代码
public void delivery(Long id) {
    Orders ordersDB = orderMapper.getById(id);

    // 校验:订单存在 且 状态必须是 3(已接单/待派送)
    if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) {
        throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
    }

    Orders orders = new Orders();
    orders.setId(ordersDB.getId());
    orders.setStatus(Orders.DELIVERY_IN_PROGRESS);  // 改成状态 4

    orderMapper.update(orders);
}

派送的前提是商家已经接单(状态3),不能跳过接单直接派送。这个状态校验就是防止「跨状态跳步」的。


完成订单 PUT /admin/order/complete/{id}

Controller 层 (OrderController)

java 复制代码
@PutMapping("/complete/{id}")
public Result complete(@PathVariable("id") Long id) {
    orderService.complete(id);
    return Result.success();
}

Service 层 (OrderServiceImpl)

java 复制代码
public void complete(Long id) {
    Orders ordersDB = orderMapper.getById(id);

    // 校验:订单存在 且 状态必须是 4(派送中)
    if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) {
        throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
    }

    Orders orders = new Orders();
    orders.setId(ordersDB.getId());
    orders.setStatus(Orders.COMPLETED);          // 改成状态 5
    orders.setDeliveryTime(LocalDateTime.now()); // 额外记录一下送达时间

    orderMapper.update(orders);
}

完成订单是状态流转的最后一步。唯一比派送多做了一件事 --- 记录了 deliveryTime(送达时间)。

8 个接口横向对比总结

接口 路径 状态变化 有前置校验 有退款逻辑 核心操作
订单搜索 GET /conditionSearch --- --- --- 分页+动态条件查询+拼装菜品串
数量统计 GET /statistics --- --- --- count(*) where status = ? 查三次
订单详情 GET /details/{id} --- --- --- 查 orders 表 + 查 order_detail 表 → 合成 VO
接单 PUT /confirm 2→3 纯 update 状态
拒单 PUT /rejection 2→6 ✅ 状态=2 ✅ 已支付就退款 校验→退款→改状态+记录原因
取消订单 PUT /cancel 任意→6 ✅ 已支付就退款 退款→改状态+记录原因
派送订单 PUT /delivery/{id} 3→4 ✅ 状态=3 校验→update 状态
完成订单 PUT /complete/{id} 4→5 ✅ 状态=4 校验→update 状态+记录送达时间

设计模式

整个商家端订单管理其实就是 一个订单状态机

复制代码
  confirm()     delivery()     complete()
[2待接单]──→[3已接单]──→[4派送中]──→[5已完成]
    │            │
    └──rejection─┘──cancel()──→[6已取消]

每个操作本质上就是:

  1. 校验当前状态是不是合法前置状态(防止状态跳跃)
  2. 如果涉及金额(拒单/取消),先退款
  3. update 新状态 + 必要的附属字段(原因、时间戳)
相关推荐
前端snow1 小时前
ai agent --- AGUI 协议,流式渲染组件
人工智能·后端
用户8181870627461 小时前
第26章 Kafka vs RocketMQ vs RabbitMQ真实选型对比
java·后端
小兔崽子去哪了1 小时前
深度学习 2 / CNN 神经网络
后端·python
rannn_1112 小时前
【Java面试八股】Java基础篇|数据类型、面向对象、关键字、反射、注解、异常、新特性、序列化、设计模式、IO
java·后端·面试
用户8356290780512 小时前
Python 实现 Word 文档域的插入与管理
后端·python
MacroZheng2 小时前
阿里又开源了一个神级 Skill 项目!
java·人工智能·后端
Ysx2 小时前
Dify Custom Tool 设计模式:一行提示词接入一张新报表
人工智能·后端
小卿噢2 小时前
malloc 成功 ≠ 你有内存:一次把 OOM 从头测到尾
linux·后端
JoyT2 小时前
Spring AI 2.0 进阶入门:Badcase、Eval 与大模型应用效果优化
后端