【苍穹外卖 Day09|订单管理与配送范围校验】

发布时间:2026/7/18 4:46:37
【苍穹外卖 Day09|订单管理与配送范围校验】 前言Day09 的实战围绕订单模块展开用户需要能查看和复用历史订单商家需要按订单状态处理业务同时系统还要在下单前判断收货地址是否处于配送范围内。这几个功能共享同一条主线订单状态必须按照业务规则流转任何状态修改前都要先完成必要校验。文章结合当前项目代码梳理用户端订单、商家端订单管理以及 5 公里配送范围校验的实现。一、先确定订单的状态与流转规则订单表中的 status 和 payStatus 分别描述业务状态与支付状态。当前项目定义如下publicstaticfinalIntegerPENDING_PAYMENT1;publicstaticfinalIntegerTO_BE_CONFIRMED2;publicstaticfinalIntegerCONFIRMED3;publicstaticfinalIntegerDELIVERY_IN_PROGRESS4;publicstaticfinalIntegerCOMPLETED5;publicstaticfinalIntegerCANCELLED6;publicstaticfinalIntegerUN_PAID0;publicstaticfinalIntegerPAID1;publicstaticfinalIntegerREFUND2;正常的状态流转为待付款 → 待接单 → 已接单 → 派送中 → 已完成。用户或商家取消订单、商家拒单时订单会进入已取消状态。这里不能只根据前端按钮判断能否操作。服务层必须重新查询订单并以数据库中的状态作为判断依据。例如商家接单时当前订单必须处于待接单状态publicvoidconfirm(Longid){OrdersordersDBrequireOrder(id);if(!ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)){thrownewOrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);}OrdersordersOrders.builder().id(ordersDB.getId()).status(Orders.CONFIRMED).build();orderMapper.update(orders);}requireOrder 负责查询订单并处理订单不存在的情况通过校验后再构造只包含待更新字段的 Orders 对象交给 Mapper。这样可以避免前端重复提交或请求过期时把订单写入错误状态。二、用户端历史订单列表、详情、取消与再来一单用户端接口统一位于/user/order下Day09 新增的核心接口包括GetMapping(/historyOrders)publicResultPageResulthistoryOrders(OrdersPageQueryDTOordersPageQueryDTO){returnResult.success(orderService.historyOrders(ordersPageQueryDTO));}GetMapping(/orderDetail/{id})publicResultOrderVOorderDetail(PathVariableLongid){returnResult.success(orderService.orderDetail(id));}PutMapping(/cancel/{id})publicResultcancel(PathVariableLongid){orderService.cancel(id);returnResult.success();}PostMapping(/repetition/{id})publicResultrepetition(PathVariableLongid){orderService.repetition(id);returnResult.success();}1. 历史订单列表为什么还要查询订单明细订单列表不仅要展示订单号、金额和下单时间还要展示菜品信息。因此订单分页查询完成后服务层会遍历当前页订单根据订单 id 查询订单明细表并将结果放进 OrderVO。publicPageResulthistoryOrders(OrdersPageQueryDTOordersPageQueryDTO){ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());PageHelper.startPage(ordersPageQueryDTO.getPage(),ordersPageQueryDTO.getPageSize());PageOrderVOpageorderMapper.pageQuery(ordersPageQueryDTO);for(OrderVOorderVO:page.getResult()){ListOrderDetaildetailsorderdetailMapper.getByOrderId(orderVO.getId());orderVO.setOrderDetailList(details);tryFillAddress(orderVO);}returnnewPageResult(page.getTotal(),page.getResult());}BaseContext.getCurrentId() 把当前登录用户 id 写入查询条件保证用户只能看自己的订单。Mapper 中的用户 id、状态、订单号、手机号和下单时间均是可选条件最后按下单时间倒序排列selectidpageQueryresultTypecom.sky.vo.OrderVOselect * from orderswhereiftestnumber ! null and number ! and number like concat(%, #{number}, %)/ififtestuserId ! nulland user_id #{userId}/ififteststatus ! nulland status #{status}/if/whereorder by order_time desc/select2. 查询详情时补齐数据并校验订单归属用户请求订单详情时不能直接用订单 id 返回数据。项目先验证订单是否属于当前用户再将订单、订单明细和地址组装为 OrderVOprivateOrdersrequireUserOrder(Longid){OrdersordersrequireOrder(id);if(!orders.getUserId().equals(BaseContext.getCurrentId())){thrownewOrderBusinessException(MessageConstant.ORDER_NOT_FOUND);}returnorders;}publicOrderVOorderDetail(Longid){returntoOrderVO(requireUserOrder(id));}这一步既是业务校验也是接口数据隔离。测试时可用 A 用户的 token 请求 B 用户的订单详情预期应返回订单不存在或业务异常而不是暴露订单内容。3. 取消订单与再来一单当前项目的取消逻辑会校验订单归属将状态改为已取消并记录取消时间当支付状态为已支付时会进入退款处理入口。当前代码以日志模拟退款申请文章不把它描述为真实微信退款已经完成。再来一单不重新创建订单而是将原订单明细转换为购物车数据再批量插入购物车表publicvoidrepetition(Longid){LonguserIdBaseContext.getCurrentId();ListOrderDetaildetailsorderdetailMapper.getByOrderId(id);ListShoppingCartcartsdetails.stream().map(detail-{ShoppingCartcartnewShoppingCart();BeanUtils.copyProperties(detail,cart,id);cart.setUserId(userId);cart.setCreateTime(LocalDateTime.now());returncart;}).collect(Collectors.toList());shoppingCartMapper.insertBatch(carts);}复制时排除原订单明细 id避免把旧记录的主键带入购物车。接口返回成功后前端跳转购物车即可让用户确认数量、口味和地址再重新提交订单。三、商家端订单管理围绕状态处理订单商家端接口前缀为 /admin/order包含订单搜索、状态数量统计、详情查询和订单状态处理。1. 搜索与状态数量统计订单搜索复用 OrdersPageQueryDTO支持订单号、手机号、状态、开始时间和结束时间等条件。分页结果中的每条订单还会补充菜品名称和数量供管理端列表直接展示。状态统计则按待接单、待派送和派送中三种状态分别查询publicOrderStatisticsVOstatistics(){OrderStatisticsVOresultnewOrderStatisticsVO();result.setToBeConfirmed(orderMapper.countByStatus(Orders.TO_BE_CONFIRMED));result.setConfirmed(orderMapper.countByStatus(Orders.CONFIRMED));result.setDeliveryInProgress(orderMapper.countByStatus(Orders.DELIVERY_IN_PROGRESS));returnresult;}对应 SQL 很简单但状态常量必须和前端标签一致selectidcountByStatusresultTypejava.lang.Integerselect count(*) from orders where status #{status}/select2. 拒单、派送和完成订单的校验差异拒单只允许待接单订单操作已支付订单需要进入退款入口并记录拒单原因和取消时间publicvoidreject(OrdersRejectionDTOdto){OrdersordersDBorderMapper.getById(dto.getId());if(ordersDBnull||!ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)){thrownewOrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);}if(ordersDB.getPayStatus().equals(Orders.PAID)){log.info(申请退款:{},ordersDB.getNumber());}orderMapper.update(Orders.builder().id(ordersDB.getId()).status(Orders.CANCELLED).rejectionReason(dto.getRejectionReason()).cancelTime(LocalDateTime.now()).build());}派送订单必须从已接单状态进入派送中完成订单必须从派送中进入已完成。把每个动作的前置状态写死在服务层订单流转才不会被任意接口跳过。操作允许的原状态更新后的状态额外信息接单待接单已接单无拒单待接单已取消拒单原因、取消时间已支付时退款派送已接单派送中无完成派送中已完成无商家取消订单存在已取消取消时间已支付时退款四、下单前增加 5 公里配送范围校验实战要求规定收货地址距离门店超过 5 公里时下单失败。当前项目将门店地址和百度地图 AK 放入环境配置再在提交订单过程中调用地图 Web 服务。公共配置文件只保留结构真实门店地址和 AK 应放到开发环境配置中提交博客时必须脱敏sky:shop:address:${sky.shop.address}baidu:ak:${sky.baidu.ak}服务层的处理顺序是调用地理编码接口把门店地址转换成经纬度用同一接口解析用户完整收货地址调用驾车路线规划接口得到路线距离当距离大于 5000 米时抛出“超出配送范围”异常只有校验通过才插入订单和订单明细。核心拦截点位于 submitOrderorders.setAddress(buildAddress(addressBook));orders.setUserId(userId);checkOutOfRange(orders.getAddress());orderMapper.insert(orders);checkOutOfRange 中先调用地理编码接口解析两个地址再调用驾车路线规划接口。路线接口的起点和终点坐标格式是“纬度,经度”项目解析首条路线的 distance 字段并以米为单位与 5000 比较。JSONObjectresultjsonObject.getJSONObject(result);JSONArrayroutes(JSONArray)result.get(routes);Integerdistance(Integer)((JSONObject)routes.get(0)).get(distance);if(distance5000){thrownewOrderBusinessException(超出配送范围);}地址解析或路线规划响应的 status 不为 0 时当前代码会抛出“店铺地址解析失败”“收货地址解析失败”或“配送路线规划失败”。这样能阻止无效地址继续创建订单也能让前端给出明确提示。五、如何测试与排查可以先通过 Swagger 接口文档逐个验证再进行前后端联调。建议按以下顺序准备数据创建一笔已支付、待接单订单验证商家接单、拒单和取消订单将订单接单后验证派送再将订单改为派送中验证完成使用当前用户查询历史订单与详情再换一个用户 token 验证订单归属校验调用再来一单检查购物车中是否新增原订单的菜品和套餐明细分别使用门店附近和明显超出范围的收货地址提交订单观察下单成功与“超出配送范围”异常。排查配送校验时优先检查门店地址是否配置、AK 是否有效、地址是否能被解析以及路线响应中是否存在 result.routes。排查订单状态异常时先查询数据库中的状态和支付状态再确认调用的接口是否满足对应动作的前置状态。总结Day09 完成的是一条完整的订单处理链路用户端能管理历史订单商家端能按状态处理订单系统在创建订单前还会根据实际驾车路线拦截超出范围的地址。实现订单模块时每一次状态变化都要有明确的前置条件、需要记录的字段和可验证的结果。