用户端历史订单模块

1. 查询历史订单

业务规则

  • 分页查询历史订单

  • 可以根据订单状态查询

  • 展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片)

@Data  
@NoArgsConstructor  
@AllArgsConstructor  
public class OrderVO extends Orders implements Serializable {  
  
    //订单菜品信息  
    private String orderDishes;  
  
    //订单详情  
    private List<OrderDetail> orderDetailList;  
  
}

在分页查询中,status 是一个可选的过滤参数。使用 Integer 类型可以将其设置为 null,表示不按状态过滤。如果使用 int 基本类型,则无法表示”无状态”的情况,因为基本类型必须有一个默认值。

public PageResult pageQuery(int pageNum, int pageSize, Integer status) {  
    OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO();  
    ordersPageQueryDTO.setStatus(status);  
    ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());  
  
    log.info("查询历史订单:{}", ordersPageQueryDTO);  
  
    PageHelper.startPage(pageNum, pageSize);  
    Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);  
    List<OrderVO> orderVOList = new ArrayList<>();  
  
    if (page != null && page.size() > 0) {  
        for (Orders orders : page) {  
            OrderVO orderVO = new OrderVO();  
            BeanUtils.copyProperties(orders, orderVO);  
            List<OrderDetail> detailList = orderDetailMapper.getByOrderId(orders.getId());  
            orderVO.setOrderDetailList(detailList);  
            orderVOList.add(orderVO);  
        }  
    }  
  
    return new PageResult(page.getTotal(), orderVOList);  
}
<select id="pageQuery" resultType="com.sky.entity.Orders">  
    select * from orders    <where>  
        <if test="status != null">  
            and status = #{status}        </if>  
        <if test="userId != null">  
            and user_id = #{userId}        </if>  
    </where>  
    order by order_time desc</select>

2. 查询订单详情

@Override  
public OrderVO details(Long orderId) {  
    Orders order = orderMapper.getById(orderId);  
    OrderVO orderVO = new OrderVO();  
    BeanUtils.copyProperties(order, orderVO);  
    List<OrderDetail> detailList = orderDetailMapper.getByOrderId(orderId);  
    orderVO.setOrderDetailList(detailList);  
    return orderVO;  
}

3. 取消订单

业务规则:

  • 待支付和待接单状态下,用户可直接取消订单

  • 商家已接单状态下,用户取消订单需电话沟通商家

  • 派送中状态下,用户取消订单需电话沟通商家

  • 如果在待接单状态下取消订单,需要给用户退款

  • 取消订单后需要将订单状态修改为“已取消”

    public void UserCancelById(Long id) {  
        // 根据id查询订单  
        Orders ordersDB = orderMapper.getById(id);  
  
        // 校验订单是否存在  
        if (ordersDB == null) {  
            throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);  
        }  
  
        //订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消  
        if (ordersDB.getStatus() > 2) {  
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);  
        }  
  
        Orders orders = new Orders();  
        orders.setId(ordersDB.getId());  
  
        // 订单处于待接单状态下取消,需要进行退款  
        if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {  
            //调用微信支付退款接口  
//            weChatPayUtil.refund(  
//                    ordersDB.getNumber(), //商户订单号  
//                    ordersDB.getNumber(), //商户退款单号  
//                    new BigDecimal(0.01),//退款金额,单位 元  
//                    new BigDecimal(0.01));//原订单金额  
  
            //支付状态修改为 退款  
            orders.setPayStatus(Orders.REFUND);  
        }  
  
        // 更新订单状态、取消原因、取消时间  
        orders.setStatus(Orders.CANCELLED);  
        orders.setCancelReason("用户取消");  
        orders.setCancelTime(LocalDateTime.now());  
        orderMapper.update(orders);  
    }

Warning

注释微信支付相关的代码

4. 再来一单

业务规则:

  • 再来一单就是将原订单中的商品重新加入到购物车中
public void repetition(Long id) {  
    // 查询当前用户id  
    Long userId = BaseContext.getCurrentId();  
  
    // 根据订单id查询当前订单详情  
    List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);  
  
    // 将订单详情对象转换为购物车对象  
    List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {  
        ShoppingCart shoppingCart = new ShoppingCart();  
  
        // 将原订单详情里面的菜品信息重新复制到购物车对象中  
        BeanUtils.copyProperties(x, shoppingCart, "id");  
        shoppingCart.setUserId(userId);  
        shoppingCart.setCreateTime(LocalDateTime.now());  
  
        return shoppingCart;  
    }).collect(Collectors.toList());  
  
    // 将购物车对象批量添加到数据库  
    shoppingCartMapper.insertBatch(shoppingCartList);  
}

商家端订单管理模块

1. 订单搜索

业务规则:

  • 输入订单号/手机号进行搜索,支持模糊搜索

  • 根据订单状态进行筛选

  • 下单时间进行时间筛选

  • 搜索内容为空,提示未找到相关订单

  • 搜索结果页,展示包含搜索关键词的内容

  • 分页展示搜索到的订单数据

/**  
 * 订单条件搜索  
 * @param ordersPageQueryDTO  
 * @return  
 */@Override  
public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {  
    PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());  
    Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);  
    List<OrderVO> orderVOList = new ArrayList<>();  
    if (page != null && page.size() > 0) {  
        for (Orders order : page) {  
            OrderVO orderVO = new OrderVO();  
            BeanUtils.copyProperties(order, orderVO);  
            orderVO.setOrderDishes(getOrderDishesStr(order));  
            orderVOList.add(orderVO);  
        }  
    }  
    return new PageResult(page.getTotal(), orderVOList);  
}  
  
/**  
 * 根据订单id获取菜品信息字符串  
 * @param order  
 * @return  
 */private String getOrderDishesStr(Orders order) {  
    // 查询订单菜品详情信息(订单中的菜品和数量)  
    List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(order.getId());  
  
    // 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)  
    List<String> orderDishList = orderDetailList.stream().map(x -> {  
        String orderDish = x.getName() + "*" + x.getNumber() + ";";  
        return orderDish;  
    }).collect(Collectors.toList());  
  
    // 将该订单对应的所有菜品信息拼接在一起  
    return String.join("", orderDishList);  
}
<select id="pageQuery" resultType="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="userId != null">  
            and user_id = #{userId}        </if>  
        <if test="status != null">  
            and status = #{status}        </if>  
        <if test="beginTime != null">  
            and order_time &gt;= #{beginTime}  
        </if>  
        <if test="endTime != null">  
            and order_time &lt;= #{endTime}  
        </if>  
    </where>  
    order by order_time desc</select>

转义字符

在MyBatis的XML映射文件中,pageQuery查询中使用&gt;&lt;是因为XML的语法要求。在XML中,><是特殊字符,有特定的含义,不能直接在XML内容中使用,需要使用对应的实体引用:

  1. &gt; 代表 >
  2. &lt; 代表 <

这是为了避免XML解析器将这些符号误解为XML标签的一部分。在OrderMapper.xml中,我们看到以下代码:

<if test="beginTime != null">
    and order_time &gt;= #{beginTime}
</if>
<if test="endTime != null">
    and order_time &lt;= #{endTime}
</if>

这实际上等价于SQL中的:

and order_time >= #{beginTime}
and order_time <= #{endTime}

至于是否有更优雅的方式,有几种替代方案:

  1. 使用CDATA段
<if test="beginTime != null">
    <![CDATA[ and order_time >= #{beginTime} ]]>
</if>
<if test="endTime != null">
    <![CDATA[ and order_time <= #{endTime} ]]>
</if>
  1. 使用转义字符(就是现在使用的方案):
<if test="beginTime != null">
    and order_time &gt;= #{beginTime}
</if>
<if test="endTime != null">
    and order_time &lt;= #{endTime}
</if>

目前项目中使用的方案(转义字符)是标准且广泛接受的做法,虽然看起来不够优雅,但是清晰明确,所有XML解析器都能正确处理。

2. 各个状态的订单数量统计

{
  "code": 0,
  "data": {
    "confirmed": 0,
    "deliveryInProgress": 0,
    "toBeConfirmed": 0
  },
  "msg": "string"
}
@Data  
public class OrderStatisticsVO implements Serializable {  
    //待接单数量  
    private Integer toBeConfirmed;  
  
    //待派送数量  
    private Integer confirmed;  
  
    //派送中数量  
    private Integer deliveryInProgress;  
}
public OrderStatisticsVO statistics() {  
    // 根据状态,分别查询出待接单、待派送、派送中的订单数量  
    Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED);  
    Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED);  
    Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS);  
  
    // 将查询出的数据封装到orderStatisticsVO中响应  
    OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO();  
    orderStatisticsVO.setToBeConfirmed(toBeConfirmed);  
    orderStatisticsVO.setConfirmed(confirmed);  
    orderStatisticsVO.setDeliveryInProgress(deliveryInProgress);  
    return orderStatisticsVO;  
}

3. 查询订单详情

业务规则:

  • 订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)
  • 订单详情页面需要展示订单明细数据(商品名称、数量、单价)
@GetMapping("/details/{id}")  
@ApiOperation("查询订单详情")  
public Result<OrderVO> details(@PathVariable("id") Long id) {  
    OrderVO orderVO = orderService.details(id);  
    return Result.success(orderVO);  
}

用户端查询订单接口共享业务层代码

4. 接单

业务规则:

  • 商家接单其实就是将订单的状态修改为“已接单”
public void confirm(OrdersConfirmDTO ordersConfirmDTO) {  
    Orders orders = Orders.builder()  
            .id(ordersConfirmDTO.getId())  
            .status(Orders.CONFIRMED)  
            .build();  
  
    orderMapper.update(orders);  
}

5. 拒单

业务规则:

  • 商家拒单其实就是将订单状态修改为“已取消”

  • 只有订单处于“待接单”状态时可以执行拒单操作

  • 商家拒单时需要指定拒单原因

  • 商家拒单时,如果用户已经完成了支付,需要为用户退款

@Data  
public class OrdersRejectionDTO implements Serializable {  
  
    private Long id;  
  
    //订单拒绝原因  
    private String rejectionReason;  
  
}

6. 取消订单

业务规则:

  • 取消订单其实就是将订单状态修改为“已取消”

  • 商家取消订单时需要指定取消原因

  • 商家取消订单时,如果用户已经完成了支付,需要为用户退款

7. 派送订单

业务规则:

  • 派送订单其实就是将订单状态修改为“派送中”

  • 只有状态为“待派送”的订单可以执行派送订单操作

public void delivery(Long id) {  
    // 根据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);  
  
    orderMapper.update(orders);  
}

8. 完成订单

业务规则:

  • 完成订单其实就是将订单状态修改为“已完成”

  • 只有状态为“派送中”的订单可以执行订单完成操作

public void complete(Long id) {  
    // 根据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);  
    orders.setDeliveryTime(LocalDateTime.now());  
  
    orderMapper.update(orders);  
}

校验收货地址是否超出配送范围

1. 环境准备

登录百度地图开放平台:百度地图开放平台

进入控制台,创建应用,获取AK:

image-1

image-2

相关接口:

2. 代码开发

2.1 application.yml

配置外卖商家店铺地址和百度地图的AK:

image-3

2.2 OrderServiceImpl

改造OrderServiceImpl,注入上面的配置项:

    @Value("${sky.shop.address}")
    private String shopAddress;
 
    @Value("${sky.baidu.ak}")
    private String ak;

在OrderServiceImpl中提供校验方法:

/**
     * 检查客户的收货地址是否超出配送范围
     * @param address
     */
    private void checkOutOfRange(String address) {
        Map map = new HashMap();
        map.put("address",shopAddress);
        map.put("output","json");
        map.put("ak",ak);
 
        //获取店铺的经纬度坐标
        String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
 
        JSONObject jsonObject = JSON.parseObject(shopCoordinate);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("店铺地址解析失败");
        }
 
        //数据解析
        JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location");
        String lat = location.getString("lat");
        String lng = location.getString("lng");
        //店铺经纬度坐标
        String shopLngLat = lat + "," + lng;
 
        map.put("address",address);
        //获取用户收货地址的经纬度坐标
        String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
 
        jsonObject = JSON.parseObject(userCoordinate);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("收货地址解析失败");
        }
 
        //数据解析
        location = jsonObject.getJSONObject("result").getJSONObject("location");
        lat = location.getString("lat");
        lng = location.getString("lng");
        //用户收货地址经纬度坐标
        String userLngLat = lat + "," + lng;
 
        map.put("origin",shopLngLat);
        map.put("destination",userLngLat);
        map.put("steps_info","0");
 
        //路线规划
        String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", map);
 
        jsonObject = JSON.parseObject(json);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("配送路线规划失败");
        }
 
        //数据解析
        JSONObject result = jsonObject.getJSONObject("result");
        JSONArray jsonArray = (JSONArray) result.get("routes");
        Integer distance = (Integer) ((JSONObject) jsonArray.get(0)).get("distance");
 
        if(distance > 5000){
            //配送距离超过5000米
            throw new OrderBusinessException("超出配送范围");
        }
    }

在OrderServiceImpl的submitOrder方法中调用上面的校验方法:

image-4