一、回顾与概览
第八天主要完成了用户下单和订单支付两处功能,第九天则是对用户下单功能进行优化,加入加入校验逻辑,如果用户的收货地址距离商家门店超出配送范围(配送范围为5公里内),则下单失败。另外需要完成用户端的历史订单模块以及商家端订单管理模块,第九天为实战内容,根据产品原型进行需求分析和接口设计,再根据接口设计进行代码实现,最后分别通过swagger接口文档和前后端联调进行功能测试。
二、用户端历史订单模块
1.产品原型和接口文档概览
(1)产品原型
查询历史订单:

查询订单详情:



取消订单:

再来一单:

(2)接口文档
历史订单查询:


查询订单详情:


取消订单:

再来一单:

2.查询历史订单
(1)需求分析和接口设计
从请求参数page和pageSize可以看出这个查询是做一个分页查询操作,还有个status通过观察产品原型可以发现有全部订单、待付款、已取消三种选项页面,推出这个status到时候就是通过一个动态SQL查询,条件status,当然还有个userId作为条件,正好这三个参数可以用自定义DTO类OrdersPageQueryDTO封装。分析返回值可以使用Orders进行封装SQL语句查询结果,需要注意orderDetailList字段是个List<OrderDetail>,OrderDetail类的所有字段属性都来自另一个表order_detail,因此需要联表查询,可以通过一次Select语句完成全部属性的封装,在.xml文件里面利用resultMap、<result>、<collection>,由于两张表一次性查存在重复字段,需要给其中一张表的查询字段添加统一前缀(别名)进行区分,防止封装时属性相互覆盖。
(2)Controller层

/**
* 历史订单查询
* @param dto
* @return
*/
@ApiOperation("历史订单查询")
@GetMapping("/historyOrders")
public Result<PageResult> historyOrders(OrdersPageQueryDTO dto) {
log.info("历史订单查询:{}",dto);
PageResult pageResult = ordersService.historyOrders(dto);
return Result.success(pageResult);
}
Controller层直接调Service层即可,分页查询返回结果为PageResult类。
(3)Service层

/**
* 历史订单查询
* @param dto
* @return
*/
@Override
public PageResult historyOrders(OrdersPageQueryDTO dto) {
// 设置分页参数
PageHelper.startPage(dto.getPage(), dto.getPageSize());
// 调用Mapper
dto.setUserId(BaseContext.getCurrentId());
Page<Orders> page = ordersMapper.list(dto);
// 返回PageResult
return new PageResult(page.getTotal(), page.getResult());
}
分页查询Service层依旧三步走,设置分页参数(请求参数DTO里面封装了传过来),调用Mapper(调用前补充字段userId,作为list方法的条件之一,只看用户自己历史订单。确定最后查出来的每条数据是用Orders类进行封装,Page泛型就写Orders,由于我这里把Orders的主表字段和联表字段orderDetailList的全部属性一次性查了封装,所以Service层比较简单,后续在商家端订单搜索接口实现遇到相似需求我会采用分两次SQL查询封装的操作,后一种方法利于Mapper层方法的复用,不过Service层逻辑会稍微复杂一些),返回新建的PageResult(这里page.getResult()其实就是得到了List<Orders>,所以Page的泛型就是查出来的每行数据究竟用什么封装,记住这一点分页查询就不会忘了)。
(4)Mapper层

<resultMap id="historyOrders" type="com.sky.entity.Orders">
<result property="id" column="id"/>
<result property="number" column="number"/>
<result property="status" column="status"/>
<result property="userId" column="user_id"/>
<result property="addressBookId" column="address_book_id"/>
<result property="orderTime" column="order_time"/>
<result property="checkoutTime" column="checkout_time"/>
<result property="payMethod" column="pay_method"/>
<result property="payStatus" column="pay_status"/>
<result property="amount" column="amount"/>
<result property="remark" column="remark"/>
<result property="phone" column="phone"/>
<result property="address" column="address"/>
<result property="userName" column="user_name"/>
<result property="consignee" column="consignee"/>
<result property="cancelReason" column="cancel_reason"/>
<result property="rejectionReason" column="rejection_reason"/>
<result property="cancelTime" column="cancel_time"/>
<result property="estimatedDeliveryTime" column="estimated_delivery_time"/>
<result property="deliveryStatus" column="delivery_status"/>
<result property="deliveryTime" column="delivery_time"/>
<result property="packAmount" column="pack_amount"/>
<result property="tablewareNumber" column="tableware_number"/>
<result property="tablewareStatus" column="tableware_status"/>
<collection property="orderDetailList" ofType="com.sky.entity.OrderDetail" columnPrefix="od_">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="image" column="image"/>
<result property="orderId" column="order_id"/>
<result property="dishId" column="dish_id"/>
<result property="setmealId" column="setmeal_id"/>
<result property="dishFlavor" column="dish_flavor"/>
<result property="number" column="number"/>
<result property="amount" column="amount"/>
</collection>
</resultMap>
<select id="list" resultMap="historyOrders">
select t1.*,t2.id as 'od_id', t2.name as 'od_name', t2.image as 'od_image', t2.order_id as 'od_order_id',
t2.dish_id as 'od_dish_id', t2.setmeal_id as 'od_setmeal_id', t2.dish_flavor as 'od_dish_flavor', t2.number as
'od_number', t2.amount as 'od_amount' from order_detail t2 join orders t1 on t1.id = t2.order_id
<where>
<if test="status != null">t1.status = #{status}</if>
<if test="userId != null">and t1.user_id = #{userId}</if>
</where>
order by t1.order_time desc
</select>
由于Orders类里面存在字段orderDetailList,它属于List<OrderDetail>,业务逻辑要求我们在一次SQL语句中把每个订单对应的每一条订单明细数据OrderDetail的每个属性都做查询填充。因此单纯的resultType已经无法满足这种业务需求(一次SQL语句,属性里面套属性),需要使用.xml文件里面的resultMap,id属性是方便SQL语句对其进行引用,type属性则是返回结果封装到哪个类。
内部就使用<result>标签先表示Orders主表字段,property属性是结果封装类里的属性名,column属性是SQL查询出的字段名(有别名优先别名,没有别名就是表中的字段名)。
然后就是专门使用resultMap的情形所在(属性里面包属性),属性OrderDetail内又包含了多个属性,至于<collection>的使用是因为每个订单Orders类对应了多个OrderDetail类,即List,property属性依然是封装类Orders的属性名,ofType属性是这个集合的泛型类(of有种里面的意思就可以记成List里面的东西是什么?),columnPrefix主要是前缀(逻辑是<collection>内部的<result>的column属性加上这个前缀columnPrefix就是SQL查询OrderDetail属性内部属性时的字段名,同上别名或者order_detail表中字段名)。观察下面的select语句可以发现resultMap引用了上面的id,然后查order_detail表的时候我把这个表查出来的每个字段都取了别名“od_xx”,大费周章的原因就是主表orders和联表order_detail表字段名有重复(如id、number….),如果放任重复直接查不取别名,那么就会出现两个表查询重复字段的结果,相互覆盖封装类的某个属性。可以发现<collection>内的<result>的column属性加上columnPrefix就刚好是select语句中给order_detail表每个字段取的别名。
联表查询一个细节是order_detail join orders(join默认左外连接,因为接口文档要求order_detail属性必须返回,orders主表属性反而不是必须的,这么做就正好可以以order_detail为主)。
后面的话就是按照status和userId动态查询,并且按照下单时间倒序排序,符合产品原型要求。
3.查询订单详情
(1)需求分析和接口设计
通过观察产品原型可以发现它就是某一个订单的详细信息,第一时间想到的就是按照订单id做查询,看到接口文档路径参数id也印证了这个想法,再看返回值类型,发现跟查询历史订单的records里面的属性如出一辙,因此可以考虑对resultMap的重复引用,两个方法区别在于返回值也不一样,如刚刚所说查询订单详情只是查询某一个订单的信息而不是分页查询那种查当前用户的全部订单。
(2)Controller层

/**
* 查看订单详情
* @param id
* @return
*/
@ApiOperation("查看订单详情")
@GetMapping("/orderDetail/{id}")
public Result<Orders> ordersDetail(@PathVariable Long id) {
log.info("查看订单详情:{}",id);
Orders orders = ordersService.ordersDetail(id);
return Result.success(orders);
}
Controller层直接调Service层即可,查询结果为一个订单的详细数据,用Orders对象封装。路径参数使用@PathVariable注解。
(3)Service层

/**
* 查看订单详情
* @param id
* @return
*/
@Override
public Orders ordersDetail(Long id) {
return ordersMapper.ordersDetail(id);
}
Service层直接返回调用Mapper层查询结果即可,相较于查询历史订单可以发现我没有传UserId,因为每个订单id就可以唯一确定一个订单,不需要担心它不是该用户的订单。
(4)Mapper层

<select id="ordersDetail" resultMap="historyOrders">
select t1.*,t2.id as 'od_id', t2.name as 'od_name', t2.image as 'od_image', t2.order_id as 'od_order_id',
t2.dish_id as 'od_dish_id', t2.setmeal_id as 'od_setmeal_id', t2.dish_flavor as 'od_dish_flavor', t2.number as
'od_number', t2.amount as 'od_amount' from order_detail t2 join orders t1 on t1.id = t2.order_id
<where>
<if test="id != null">t1.id = #{id}</if>
</where>
order by t1.order_time desc
</select>
可以发现Mapper层方法几乎跟查询历史订单的Mapper层方法没有区别,除了条件参数有些不同,以及返回值是Orders,resultMap也是复用之前查询历史订单时定义的。(SQL语句几乎相同但是返回值一个是Orders一个是List<Orders>归根结底是因为查询订单详情条件参数是订单Id,这样就唯一确定了某个特定的订单,而查询历史订单是根据status和userId两个条件参数,这两个参数无法确定某个特定的订单而是某群特定的订单群,也符合Page<Orders>)。
4.取消订单
(1)需求分析和接口设计
观察产品原型发现取消订单就是一个选项按钮,再看接口文档请求参数只有一个订单Id,请求方式PUT按照规范应该是修改操作,联系数据库表Orders有个字段status,那推测大概就是根据订单Id修改status为已取消,然后设置一下订单取消时间即可,就是一个Mapper层的update操作。


(2)Controller层

/**
* 取消订单
* @return
*/
@ApiOperation("取消订单")
@PutMapping("/cancel/{id}")
public Result cancel(@PathVariable Long id) {
log.info("取消订单:{}", id);
ordersService.cancel(id);
return Result.success();
}
直接调用Service层方法就行。
(3)Service层

/**
* 取消订单
* @param id
*/
@Override
public void cancel(Long id) {
Orders orders = new Orders();
orders.setId(id);
orders.setStatus(Orders.CANCELLED);
orders.setCancelTime(LocalDateTime.now());
ordersMapper.update(orders);
}
Service层主要就是构造请求参数封装到Orders类,按照需求分析需要根据订单Id做更新Status和CancelTime的操作,这里Mapper层的update主要是复用之前的在.xml文件里面写的动态SQL。
(4)Mapper层

<update id="update" parameterType="com.sky.entity.Orders">
update orders
<set>
<if test="cancelReason != null and cancelReason!='' ">
cancel_reason=#{cancelReason},
</if>
<if test="rejectionReason != null and rejectionReason!='' ">
rejection_reason=#{rejectionReason},
</if>
<if test="cancelTime != null">
cancel_time=#{cancelTime},
</if>
<if test="payStatus != null">
pay_status=#{payStatus},
</if>
<if test="payMethod != null">
pay_method=#{payMethod},
</if>
<if test="checkoutTime != null">
checkout_time=#{checkoutTime},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="deliveryTime != null">
delivery_time = #{deliveryTime}
</if>
</set>
where id = #{id}
</update>
这里的的动态SQL语句在后面写管理端的订单状态修改也会再次用到,当时自己写就没看前面的直接另写方法,写到后面才发现可以进行优化。
5.再来一单
(1)需求分析和接口设计
说实话一开始看这个再来一单我完全不清楚是什么业务逻辑,产品原型只有一个按钮,接口文档POST请求,推测是新增但是又不知道往哪里新增数据。后面直接打开自己美团APP再来一单,发现原来是把历史订单的东西加入到购物车里面。我又想起购物车表字段和订单明细表的字段几乎一样,所以就推测是根据请求参数订单Id,查询它关联的多个订单明细数据,然后逐一插入shopping_cart表中。


(2)Controller层

/**
* 再来一单
* @return
*/
@ApiOperation("再来一单")
@PostMapping("/repetition/{id}")
public Result repetition(@PathVariable Long id) {
ordersService.repetition(id);
return Result.success();
}
直接调用Service层即可。
(3)Service层

/**
* 再来一单
* @param id
*/
@Override
public void repetition(Long id) {
// 根据订单id把对应的所有订单明细表数据加入shopping_cart表中
List<OrderDetail> orderDetailList = orderDetailMapper.selectByOrderId(id);
orderDetailList.forEach(orderDetail -> {
// 补充缺失的属性值
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(orderDetail, shoppingCart, "id");
shoppingCart.setUserId(BaseContext.getCurrentId());
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
});
}
现根据订单Id查询该订单对应的所有订单明细数据List<OrderDetail>,再遍历每个OrderDetail,每次都新建一个ShoppingCart类,拷贝补充属性值后插入到shopping_cart表中。
(4)Mapper层

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

@Insert("insert into shopping_cart (name, image, user_id, dish_id, setmeal_id, dish_flavor, amount, create_time, number) " +
"values (#{name},#{image},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{amount},#{createTime},#{number})")
void insert(ShoppingCart shoppingCart);
Mapper层就是两个SQL语句一个根据orderId查OrderDetail,一个插入ShoppingCart。
三、商家端订单管理模块
1.产品原型和接口文档概览
(1)产品原型
全部订单:

待接单:

待派送:

派送中:

已完成:

已取消:

页面说明:

订单状态:

订单详情、拒单与取消:

(2)接口文档
订单搜索:


各个状态的订单数量统计:

查询订单详情:


接单:

拒单:

取消订单:

派送订单:

完成订单:

2.订单搜索
(1)需求分析和接口设计
观察订单管理六种页面,发现上面都有订单号、手机号、下单时间范围三个选择框,配合上右边的查询可以推测出是一个条件分页查询。再看接口文档请求参数包含page、pageSize确定就是一个分页查询。请求参数还有number(订单号)、phone(手机号)、beginTime和endTime(下单时间范围),当然还有个status,这个其实就对应了订单管理六种页面,全部订单、待接单、待派送、派送中、已完成、已取消,切换不同页面请求参数status的值也就不同。records里面的数据用Orders类封装是没问题,但需要注意又多了个字段orderDishes,解释说明是订单包含的菜品,以字符串形式展示,相当于是要把每个订单包含的菜品以及它包含的套餐下包含的菜品的名字全部拼接起来作为orderDishes字段,这步操作估摸着就是在Service层里面额外处理一下,比普通的分页查询要多一步处理逻辑。
(2)Controller层

/**
* 订单搜索
* @param dto
* @return
*/
@ApiOperation("订单搜索")
@GetMapping("/conditionSearch")
public Result<PageResult> conditionSearch(OrdersPageQueryDTO dto) {
log.info("订单搜索:{}",dto);
PageResult pageResult = ordersService.conditionSearch(dto);
return Result.success(pageResult);
}
Controller层直接调用Service层就行了,分页查询返回结果固定pageResult。
(3)Service层

/**
* 订单搜索
* @param dto
* @return
*/
@Override
public PageResult conditionSearch(OrdersPageQueryDTO dto) {
// 设置分页参数
PageHelper.startPage(dto.getPage(), dto.getPageSize());
// 调用Mapper
Page<Orders> page = ordersMapper.conditionSearch(dto);
// 补充orderDishes字段,把每个订单需要的菜品的名称拼接成一个字符串
List<Orders> result = page.getResult();
result.forEach(orders -> {
List<OrderDetail> orderDetailList = orderDetailMapper.selectByOrderId(orders.getId());
List<String> orderDishNames = new ArrayList<>();
orderDetailList.forEach(orderDetail -> {
// 从order_detail表中获取订单的dish_id和setmeal_id
Long dishId = orderDetail.getDishId();
Long setmealId = orderDetail.getSetmealId();
// 根据dish_id查表dish获取dish_name
if (dishId != null) {
Dish dish = dishMapper.selectById(dishId);
orderDishNames.add(dish.getName());
}
// 根据setmeal_id查setmeal_dish表中的name(也是菜品名称)
if (setmealId != null) {
List<SetmealDish> setmealDishes = setmealDishMapper.selectBySetmealId(setmealId);
setmealDishes.forEach(setmealDish -> {
String name = setmealDish.getName();
orderDishNames.add(name);
});
}
});
// 拼接所有涉及到的菜品名称
String orderDishes = String.join(",", orderDishNames);
// set进orderDishes字段
orders.setOrderDishes(orderDishes);
});
return new PageResult(page.getTotal(), page.getResult());
}
一开始就是经典的分页查询三步走,先设置分页参数,然后调用Mapper返回Page,泛型里面就是每一行数据封装到Orders类中,但是仅仅通过conditionSearch一个方法无法封装orderDishes字段,因此需要遍历每一个订单,获取该订单包含的所有菜品名称拼接在一起(包括套餐下的菜品)。
首先就是通过getResult方法获取到订单集合List<Orders>,然后遍历所有订单进行处理。一个订单对应多个订单明细,所以需要获通过selectByOrderId方法取到订单对应所有的订单明细数据。同时在遍历该订单的所有订单明细数据前创建一个orderDishNames集合,存储该订单包含的所有菜品名称。接着就如需求分析时说的,通过orderDetail获取到dish_id和setmeal_id,据此分别查询该订单包含的所有菜品名称并加入到集合orderDishNames中。遍历结束后通过String.join方法拼接所有涉及到的菜品名称,最后setOrderDishes补充这个缺失属性,每遍历一个订单Orders都重复上述操作。
整个result.forEach都是对Page中的数据进行修正补充,保证最后返回的PageResult是正确的。
(4)Mapper层
conditionSearch:根据订单号、订单状态、手机号、时间范围动态查询

<select id="conditionSearch" resultType="com.sky.entity.Orders">
select * from orders
<where>
<if test="number != null and number != ''">number = #{number}</if>
<if test="status != null">and status = #{status}</if>
<if test="phone != null and phone != ''">and phone = #{phone}</if>
<if test="beginTime != null and endTime != null">and order_time between #{beginTime} and #{endTime}</if>
</where>
order by order_time desc
</select>
selectByOrderId:根据订单Id查该订单对应的所有订单明细数据

@Select("select * from order_detail where order_id = #{orderId}")
List<OrderDetail> selectByOrderId(Long orderId);
selectById:根据菜品Id查询对应菜品信息

@Select("select * from dish where id = #{id}")
Dish selectById(Long id);
selectBySetmealId:根据套餐Id查它包含的所有菜品名称,setmealDish封装有菜品名称

@Select("select * from setmeal_dish where setmeal_id = #{id}")
List<SetmealDish> selectBySetmealId(Long id);

3.各个状态的订单数量统计
(1)需求分析和接口设计
观察产品原型可以发现订单管理页面每个订单状态选项后面都有(数字),这个数字就是该状态的订单的数量,也就是该接口使用的地方,为了验证想法我检查管理端页面网络请求,发现每次点击到订单管理相关页面(包括各个订单状态)都会调用statistics这个请求,可见每个数字都会实时更新的,接口没有请求参数,只要求返回待派送、派送中、待接单三个订单状态数据各自的数量,相当于调用三次Mapper层方法做三次查询封装到实体类OrderStatisticsVO返回即可。
(2)Controller层

/**
*
* 各个状态的订单数量统计
* @return
*/
@ApiOperation("各个状态的订单数量统计")
@GetMapping("/statistics")
public Result<OrderStatisticsVO> statistics() {
log.info("各个状态的订单数量统计");
OrderStatisticsVO vo = ordersService.statistics();
return Result.success(vo);
}
直接调用Service层,三个状态订单数量封装到OrderStatisticsVO返回即可。
(3)Service层

@Override
public OrderStatisticsVO statistics() {
// 统计不同状态订单各自的数量
Orders orders = new Orders();
// 1.待派送数量
orders.setStatus(Orders.CONFIRMED);
Integer confirmed = ordersMapper.statistics(orders);
// 2.派送中数量
orders.setStatus(Orders.DELIVERY_IN_PROGRESS);
Integer deliveryInProgress = ordersMapper.statistics(orders);
// 3.待接单数量
orders.setStatus(Orders.TO_BE_CONFIRMED);
Integer toBeConfirmed = ordersMapper.statistics(orders);
// 4.封装进OrderStatisticsVO中
return OrderStatisticsVO.builder()
.confirmed(confirmed)
.deliveryInProgress(deliveryInProgress)
.toBeConfirmed(toBeConfirmed)
.build();
}
在查询三种不同状态订单各自的数量时我调用了三次同一个Mapper层方法statistics,不过每次调用前都会对请求参数Orders类进行set操作,即设置当前查询哪种状态的订单数量,最后通过链式编程return OrderStatisticsVO。
(4)Mapper层

/**
* 各个状态的订单数量统计
* @return
*/
@Select("select count(*) from orders where status = #{status}")
Integer statistics(Orders orders);
根据订单状态进行查询,订单状态是Service层封装好传过来的。



