Java项目苍穹外卖:接口开发(4)

用户端历史订单

查询历史订单

产品原型

业务规则

  • 分页查询历史订单
  • 可以根据订单状态查询
  • 展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片)

接口设计

代码实现

1.user/OrderController

1
2
3
4
5
6
7
//历史订单查询:订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 :
@GetMapping("/historyOrders")
@ApiOperation("历史订单查询")
public Result<PageResult> page(int page, int pageSize, Integer status) {
PageResult pageResult = orderService.pageQuery4User(page, pageSize, status);
return Result.success(pageResult);
}

2.OrderService

1
2
//用户端订单分页查询
PageResult pageQuery4User(int page, int pageSize, Integer status);

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//用户端订单分页查询
public PageResult pageQuery4User(int pageNum, int pageSize, Integer status) {
// 设置分页
PageHelper.startPage(pageNum, pageSize);

OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO();
ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());
ordersPageQueryDTO.setStatus(status);

// 分页条件查询(根据UserId查询当前用户的所有订单)
Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

List<OrderVO> list = new ArrayList();

// 查询出订单明细,并封装入OrderVO进行响应
if (page != null && page.getTotal() > 0) {
for (Orders orders : page) {
Long orderId = orders.getId();// 订单id

// 查询订单明细
List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId);

OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
orderVO.setOrderDetailList(orderDetails);

list.add(orderVO);
}
}
return new PageResult(page.getTotal(), list);
}

4.OrderMapper

1
2
//分页条件查询并按下单时间排序
Page<Orders> pageQuery(OrdersPageQueryDTO ordersPageQueryDTO);

5.OrderMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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>

6.OrderDetailMapper

1
2
3
//根据订单id查询订单明细
@Select("select * from order_detail where order_id = #{orderId}")
List<OrderDetail> getByOrderId(Long orderId);

查询订单详情

产品原型

接口设计

代码实现

1.user/OrderController

1
2
3
4
5
6
7
//查询订单详情
@GetMapping("/orderDetail/{id}")
@ApiOperation("查询订单详情")
public Result<OrderVO> details(@PathVariable("id") Long id) {
OrderVO orderVO = orderService.details(id);
return Result.success(orderVO);
}

2.OrderService

1
2
//查询订单详情
OrderVO details(Long id);

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//查询订单详情
public OrderVO details(Long id) {
// 根据id查询订单
Orders orders = orderMapper.getById(id);

// 查询该订单对应的菜品/套餐明细
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

// 将该订单及其详情封装到OrderVO并返回
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
orderVO.setOrderDetailList(orderDetailList);

return orderVO;
}

4.OrderMapper

1
2
3
//根据id查询订单
@Select("select * from orders where id = #{id}")
Orders getById(Long id);

问题

【解决配送地址为null】

参考链接:

解决黑马苍穹外卖订单详情页面缺少配送地址(address)、订单备注(remark)的问题,以及如何隐藏顾客名字和电话号码-CSDN博客

苍穹外卖:解决订单没有地址(address)的问题_mars3d的getaddress方法返回未查询到相关结果!-CSDN博客

取消订单

产品原型

业务规则:

  • 待支付和待接单状态下,用户可直接取消订单
  • 商家已接单状态下,用户取消订单需电话沟通商家
  • 派送中状态下,用户取消订单需电话沟通商家
  • 如果在待接单状态下取消订单,需要给用户退款
  • 取消订单后需要将订单状态修改为“已取消”

接口设计

代码实现

1.user/OrderController

1
2
3
4
5
6
7
//用户取消订单
@PutMapping("/cancel/{id}")
@ApiOperation("取消订单")
public Result cancel(@PathVariable("id") Long id) throws Exception {
orderService.userCancelById(id);
return Result.success();
}

2.OrderService

1
2
//用户取消订单
void userCancelById(Long id) throws Exception;

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//用户取消订单
public void userCancelById(Long id) throws Exception {
// 根据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);
}

再来一单

产品原型

接口设计

业务规则:

  • 再来一单就是将原订单中的商品重新加入到购物车中

代码实现

1.user/OrderController

1
2
3
4
5
6
7
//再来一单
@PostMapping("/repetition/{id}")
@ApiOperation("再来一单")
public Result repetition(@PathVariable Long id) {
orderService.repetition(id);
return Result.success();
}

2.OrderService

1
2
//再来一单
void repetition(Long id);

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//再来一单
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);
}

4.ShoppingCartMapper

1
2
3
4
5
6
/**
* 批量插入购物车数据
*
* @param shoppingCartList
*/
void insertBatch(List<ShoppingCart> shoppingCartList);

5.ShoppingCartMapper.xml

1
2
3
4
5
6
7
8
<insert id="insertBatch" parameterType="list">
insert into shopping_cart
(name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)
values
<foreach collection="shoppingCartList" item="sc" separator=",">
(#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime})
</foreach>
</insert>

商家端订单管理模块

订单搜索

产品原型

业务规则:

  • 输入订单号/手机号进行搜索,支持模糊搜索
  • 根据订单状态进行筛选
  • 下单时间进行时间筛选
  • 搜索内容为空,提示未找到相关订单
  • 搜索结果页,展示包含搜索关键词的内容
  • 分页展示搜索到的订单数据

接口设计

代码实现

1.admin/OrderController

admin包下创建OrderController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//订单管理
@RestController("adminOrderController")
@RequestMapping("/admin/order")
@Slf4j
@Api(tags = "订单管理接口")
public class OrderController {

@Autowired
private OrderService orderService;

//订单搜索
@GetMapping("/conditionSearch")
@ApiOperation("订单搜索")
public Result<PageResult> conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO);
return Result.success(pageResult);
}
}

2.OrderService

1
2
//条件搜索订单
PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO);

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//订单搜索
public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());

Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

// 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO
List<OrderVO> orderVOList = getOrderVOList(page);

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

private List<OrderVO> getOrderVOList(Page<Orders> page) {
// 需要返回订单菜品信息,自定义OrderVO响应结果
List<OrderVO> orderVOList = new ArrayList<>();

List<Orders> ordersList = page.getResult();
if (!CollectionUtils.isEmpty(ordersList)) {
for (Orders orders : ordersList) {
// 将共同字段复制到OrderVO
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
String orderDishes = getOrderDishesStr(orders);

// 将订单菜品信息封装到orderVO中,并添加到orderVOList
orderVO.setOrderDishes(orderDishes);
orderVOList.add(orderVO);
}
}
return orderVOList;
}

/**
* 根据订单id获取菜品信息字符串
*
* @param orders
* @return
*/
private String getOrderDishesStr(Orders orders) {
// 查询订单菜品详情信息(订单中的菜品和数量)
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

// 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
List<String> orderDishList = orderDetailList.stream().map(x -> {
String orderDish = x.getName() + "*" + x.getNumber() + ";";
return orderDish;
}).collect(Collectors.toList());

// 将该订单对应的所有菜品信息拼接在一起
return String.join("", orderDishList);
}

各个状态的订单数量统计

产品原型

接口设计

代码实现

1.admin/OrderController

1
2
3
4
5
6
7
//各个状态的订单数量统计
@GetMapping("/statistics")
@ApiOperation("各个状态的订单数量统计")
public Result<OrderStatisticsVO> statistics() {
OrderStatisticsVO orderStatisticsVO = orderService.statistics();
return Result.success(orderStatisticsVO);
}

2.OrderService

1
2
//各个状态的订单数量统计
OrderStatisticsVO statistics();

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//各个状态的订单数量统计
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;
}

4.OrderMapper

1
2
3
//根据状态统计订单数量
@Select("select count(id) from orders where status = #{status}")
Integer countStatus(Integer status);

查询订单详情

产品原型

业务规则:

  • 订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)
  • 订单详情页面需要展示订单明细数据(商品名称、数量、单价)

接口设计

代码实现

admin/OrderController

1
2
3
4
5
6
7
//订单详情
@GetMapping("/details/{id}")
@ApiOperation("查询订单详情")
public Result<OrderVO> details(@PathVariable("id") Long id) {
OrderVO orderVO = orderService.details(id);
return Result.success(orderVO);
}

接单

产品原型

业务规则:

  • 商家接单其实就是将订单的状态修改为“已接单”

接口设计

代码实现

1.admin/OrderController

1
2
3
4
5
6
7
//接单
@PutMapping("/confirm")
@ApiOperation("接单")
public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) {
orderService.confirm(ordersConfirmDTO);
return Result.success();
}

2.OrderService

1
2
//接单
void confirm(OrdersConfirmDTO ordersConfirmDTO);

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
//接单
public void confirm(OrdersConfirmDTO ordersConfirmDTO) {
Orders orders = Orders.builder()
.id(ordersConfirmDTO.getId())
.status(Orders.CONFIRMED)
.build();

orderMapper.update(orders);
}

拒单

产品原型

业务规则:

  • 商家拒单其实就是将订单状态修改为“已取消”
  • 只有订单处于“待接单”状态时可以执行拒单操作
  • 商家拒单时需要指定拒单原因
  • 商家拒单时,如果用户已经完成了支付,需要为用户退款

接口设计

代码实现

1.admin/OrderController

1
2
3
4
5
6
7
//拒单
@PutMapping("/rejection")
@ApiOperation("拒单")
public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception {
orderService.rejection(ordersRejectionDTO);
return Result.success();
}

2.OrderService

1
2
//拒单
void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception;

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//拒单
public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
// 根据id查询订单
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) {
//用户已支付,需要退款
String refund = weChatPayUtil.refund(
ordersDB.getNumber(),
ordersDB.getNumber(),
new BigDecimal(0.01),
new BigDecimal(0.01));
log.info("申请退款:{}", refund);
}

// 拒单需要退款,根据订单id更新订单状态、拒单原因、取消时间
Orders orders = new Orders();
orders.setId(ordersDB.getId());
orders.setStatus(Orders.CANCELLED);
orders.setRejectionReason(ordersRejectionDTO.getRejectionReason());
orders.setCancelTime(LocalDateTime.now());

orderMapper.update(orders);
}

取消订单

产品原型

业务规则:

  • 取消订单其实就是将订单状态修改为“已取消”
  • 商家取消订单时需要指定取消原因
  • 商家取消订单时,如果用户已经完成了支付,需要为用户退款

接口设计

代码实现

1.admin/OrderController

1
2
3
4
5
6
7
//取消订单
@PutMapping("/cancel")
@ApiOperation("取消订单")
public Result cancel(@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception {
orderService.cancel(ordersCancelDTO);
return Result.success();
}

2.OrderService

1
2
//商家取消订单
void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception;

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//取消订单
public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId());

//支付状态
Integer payStatus = ordersDB.getPayStatus();
if (payStatus == 1) {
//用户已支付,需要退款
String refund = weChatPayUtil.refund(
ordersDB.getNumber(),
ordersDB.getNumber(),
new BigDecimal(0.01),
new BigDecimal(0.01));
log.info("申请退款:{}", refund);
}

// 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间
Orders orders = new Orders();
orders.setId(ordersCancelDTO.getId());
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason(ordersCancelDTO.getCancelReason());
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}

派送订单

产品原型

业务规则:

  • 派送订单其实就是将订单状态修改为“派送中”
  • 只有状态为“待派送”的订单可以执行派送订单操作

接口设计

代码实现

1.admin/OrderController

1
2
3
4
5
6
7
//派送订单
@PutMapping("/delivery/{id}")
@ApiOperation("派送订单")
public Result delivery(@PathVariable("id") Long id) {
orderService.delivery(id);
return Result.success();
}

2.OrderService

1
2
//派送订单
void delivery(Long id);

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//派送订单
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);
}

完成订单

产品原型

业务规则:

  • 完成订单其实就是将订单状态修改为“已完成”
  • 只有状态为“派送中”的订单可以执行订单完成操作

接口设计

代码实现

1.admin/OrderController

1
2
3
4
5
6
7
//完成订单
@PutMapping("/complete/{id}")
@ApiOperation("完成订单")
public Result complete(@PathVariable("id") Long id) {
orderService.complete(id);
return Result.success();
}

2.OrderService

1
2
//完成订单
void complete(Long id);

3.OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//完成订单
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);
}

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

环境准备

注册账号:https://passport.baidu.com/v2/?reg&tt=1671699340600&overseas=&gid=CF954C2-A3D2-417F-9FE6-B0F249ED7E33&tpl=pp&u=https%3A%2F%2Flbsyun.baidu.com%2Findex.php%3Ftitle%3D%E9%A6%96%E9%A1%B5

登录百度地图开放平台:https://lbsyun.baidu.com/

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

相关接口:

https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding

https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1

代码开发

1.application.yml

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

2.OrderServiceImpl

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

1
2
3
4
5
@Value("${sky.shop.address}")
private String shopAddress;

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

OrderServiceImpl中提供校验方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//检查客户的收货地址是否超出配送范围
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("超出配送范围");
}
}

OrderServiceImplsubmitOrder方法中调用上面的校验方法:

订单状态定时处理

Spring Task

介绍

Spring TaskSpring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。

定位:定时任务框架。

作用:定时自动执行某段Java代码。

应用场景:

  • 信用卡每月还款提醒。
  • 银行贷款每月还款提醒。
  • 火车票售票系统处理未支付订单。
  • 入职纪念日为用户发送通知。

只要是需要定时处理的场景都可以使用Spring Task

cron表达式

cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间。

构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义。

每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)。

cron表达式在线生成器:https://cron.qqe2.com/

入门案例

Spring Task使用步骤:

导入maven坐标spring-context(已存在)。

2.启动类添加注解@EnableScheduling开启任务调度。

1
2
@EnableScheduling//开启任务调度
public class SkyApplication {}

3.自定义定时任务类。

1
2
3
4
5
6
7
8
9
10
11
12
package com.sky.task;

//自定义定时任务类
@Component
@Slf4j
public class MyTask {
//定时任务:每隔5秒触发一次
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask(){
log.info("定时任务开始执行:{}", new Date());
}
}

订单状态定时处理

需求分析

用户下单后可能存在的情况:

  • 下单后未支付,订单一直处于“待支付”状态。
  • 用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态。

对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:

  • 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”。
  • 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”。

代码开发

OrderMapper接口中扩展方法。

1
2
3
//根据订单状态和下单时间查询订单
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);

自定义定时任务类OrderTask。完善定时任务类的processTimeoutOrder方法。完善定时任务类的processDeliveryOrder方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.sky.task;

//定时任务类,定时处理订单状态
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderMapper orderMapper;

//处理超时订单的方法
@Scheduled(cron = "0 * * * * *")//每分钟触发一次
public void processTimeoutOrder(){
log.info("定时处理超时订单:{}", LocalDateTime.now());

LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
//处理15分钟内未支付的订单

//select * from orders where status = ? and order_time < (当前时间-15)
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);

if(ordersList != null && ordersList.size() > 0){
for(Orders orders : ordersList){//取消超时订单
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("订单超时,自动取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
}

//处理一直处于派送中状态的订单
@Scheduled(cron = "0 0 1 * * ?")//每天凌晨1点触发一次
public void processDeliveryOrder(){
log.info("定时处理处于派送中的订单:{}", LocalDateTime.now());

LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
//凌晨1点的时候处理前一个小时的订单,也就是前一天派送中的订单

List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);

if(ordersList != null && ordersList.size() > 0){
for(Orders orders : ordersList){//将前一天所有处于派送中的订单设置为完成
orders.setStatus(Orders.CONFIRMED);
orderMapper.update(orders);
}
}
}
}

WebSocket

介绍

WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信:浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。

HTTP协议和WebSocket协议对比:

  1. HTTP是短连接,WebSocket是长连接。
  2. HTTP通信是单向的,基于请求响应模式。WebSocket支持双向通信。
  3. HTTPWebSocket底层都是TCP连接。

WebSocket应用场景:

  • 视频弹幕
  • 网页聊天
  • 体育实况更新
  • 股票基金报价实时更新

WebSocket缺点:

  • 服务器长期维护长连接需要一定的成本。
  • 各个浏览器支持程度不一。
  • WebSocket是长连接,受网络限制比较大,需要处理好重连。

结论:WebSocket支持双向通信,功能看似比HTTP强大,但是也不可以基于WebSocket开发所有的业务功能。WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用。

入门案例

实现步骤:

1.直接使用websocket.html页面作为WebSocket客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
</head>
<body>
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<button onclick="closeWebSocket()">关闭连接</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
var clientId = Math.random().toString(36).substr(2);

//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//连接WebSocket节点
websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
}
else{
alert('Not support websocket')
}

//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};

//连接成功建立的回调方法
websocket.onopen = function(){
setMessageInnerHTML("连接成功");
}

//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}

//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}

//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}

//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}

//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}

//关闭连接
function closeWebSocket() {
websocket.close();
}
</script>
</html>

2.导入WebSocketmaven坐标。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

3.导入WebSocket服务端组件WebSocketServer,用于和客户端通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.sky.websocket;

//WebSocket服务
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();

//连接建立成功调用的方法
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}

//收到客户端消息后调用的方法,参数:message 客户端发送过来的消息
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}

//连接关闭调用的方法
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}

//群发
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

4.导入配置类WebSocketConfiguration,注册WebSocket的服务端组件。

1
2
3
4
5
6
7
8
9
10
package com.sky.config;

//WebSocket配置类,用于注册WebSocket的Bean
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

5.导入定时任务类WebSocketTask,定时向客户端推送数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.sky.task;

@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;

//通过WebSocket每隔5秒向客户端发送消息
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}
}

6.运行结果。

来单提醒

需求分析

用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式有如下两种:

  • 语音播报。
  • 弹出提示框。

设计

1.通过WebSocket实现管理端页面和服务端保持长连接状态。

2.当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息。

3.客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报。

4.约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:typeorderIdcontent

  • type为消息类型:1为来单提醒,2为客户催单。
  • orderId为订单id
  • content 为消息内容。

代码开发

OrderServiceImpl中注入WebSocketServer对象,修改paySuccess方法,加入如下代码:(这是使用了微信支付的)

1
2
3
4
5
6
Map map = new HashMap();
map.put("type", 1);//通知类型 1来单提醒 2客户催单
map.put("orderId", orders.getId());//订单id
map.put("content","订单号:" + outTradeNo);

webSocketServer.sendToAllClient(JSON.toJSONString(map));

如果跳过微信支付,需要改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Autowired
private WebSocketServer webSocketServer;

//订单支付
@Override
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception{
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(userId);

//调用微信支付接口,生成预支付交易单
/*JSONObject jsonObject = weChatPayUtil.pay(
ordersPaymentDTO.getOrderNumber(), //商户订单号
new BigDecimal(0.01), //支付金额,单位 元
"苍穹外卖订单", //商品描述
user.getOpenid() //微信用户的openid
);

if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
throw new OrderBusinessException("该订单已支付");
}*/

JSONObject jsonObject = new JSONObject();
jsonObject.put("code", "ORDERPAID");
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));

//为替代微信支付成功后的数据库订单状态更新,多定义一个方法进行修改
Integer OrderPaidStatus = Orders.PAID; //支付状态,已支付
Integer OrderStatus = Orders.TO_BE_CONFIRMED; //订单状态,待接单

//发现没有将支付时间 check_out属性赋值,所以在这里更新
LocalDateTime check_out_time = LocalDateTime.now();

//获取订单号码
String orderNumber = ordersPaymentDTO.getOrderNumber();

log.info("调用updateStatus,用于替换微信支付更新数据库状态的问题");
orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time, orderNumber);

// 补充:支付成功之后进行来单提醒。
// 本来是支付成功后会调用com.sky.controller.notify下回调方法paySuccessNotify,该方法调用了paySuccess,
// 应该在com.sky.service.impl下的OrderServiceImpl的paySuccess中写,但是因为跳过了微信支付,这里直接下单成功进行来单提醒。

// 通过websocket向客户端浏览器推送消息 type orderId content
Orders orders = orderMapper.getByNumber(orderNumber);
Map map = new HashMap();
map.put("type", 1);
map.put("orderId", orders.getId());
map.put("content", "订单号:" + orderNumber);

String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);

return vo;
}

注意

浏览器发起的请求为:

1
2
3
请求 URL: ws://localhost/ws/3lkr84icci
Request Method: GET
状态代码: 101 Switching Protocols

请求url中未指定端口,但是nginx中配置了反向代理指定了8080端口,所有能够顺利访问管理端。

问题

【没有语音播报】

解决:浏览器退出登录,重新登录账号。

【语音播报一直循环】

解决:注释掉WebSocketTask中的sendMessageToClient方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sky.task;

@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;

//通过WebSocket每隔5秒向客户端发送消息
/* @Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}*/
//注释掉防止一直播放语音
}

客户催单

需求分析

用户在小程序中点击催单按钮后,需要第一时间通知外卖商家。通知的形式有如下两种:

  • 语音播报。
  • 弹出提示框。

设计

1.通过WebSocket实现管理端页面和服务端保持长连接状态。

2.当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息。

3.客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报。

4.约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:typeorderIdcontent

  • type为消息类型:1为来单提醒,2为客户催单。
  • orderId为订单id
  • content 为消息内容。

接口设计

代码开发

1.根据用户催单的接口定义,在user/OrderController中创建催单方法。

1
2
3
4
5
6
7
//客户催单
@GetMapping("/reminder/{id}")
@ApiOperation("客户催单")
public Result reminder(@PathVariable("id") Long id) {
orderService.reminder(id);
return Result.success();
}

2.在OrderService接口中声明reminder方法。

1
2
//客户催单
void reminder(Long id);

3.在OrderServiceImpl中实现reminder方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//客户催单
@Override
public void reminder(Long id) {
//根据id查询订单
Orders ordersDB = orderMapper.getById(id);

//校验订单是否存在
if(ordersDB == null){
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}

Map map = new HashMap();
map.put("type", 2);//1表示来单提醒,2表示客户催单
map.put("orderId", id);
map.put("content", "订单号:" + ordersDB.getNumber());

//通过websocket向客户端浏览器推送消息
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
}

Java项目苍穹外卖:接口开发(4)
http://surourou8.github.io/2024/11/21/Java项目苍穹外卖:接口开发(4)/
作者
Su Rourou
发布于
2024年11月21日
许可协议