Day06-05~12微信小程序
HttpClient
HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
HttpClient的maven坐标:
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency>
HttpClient的核心API:
-
HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
-
HttpClients:可认为是构建器,可创建HttpClient对象。
-
CloseableHttpClient:实现类,实现了HttpClient接口。
-
HttpGet:Get方式请求类型。
-
HttpPost:Post方式请求类型。
HttpClient发送请求步骤:
-
创建HttpClient对象
-
创建Http请求对象
-
调用HttpClient的execute方法发送请求
入门样例测试
复制黑马的代码还报错,应该是太老了,还得自己敲
这个需要把基础库版本改到2.27.1以下. 详情->本地设置->调试基础库
第二次成功,返回数据为:
一个授权码(js_code)只能使用一次
主要就是得到openid
{"session_key":"jV+yNYfBjUvoGHFunQzJgA==","openid":"oK0ST89FECmTNZqeWIKCZ_AkiPl4"}
第二次发送返回
{"errcode":40163,"errmsg":"code been used, rid: 66dc273c-1da05619-46a3d447"}
Day06-14微信登录
个人开发小程序不能获取用户手机号
微信登录业务层设计与分析
-
调用微信的接口服务,获得当前用户的openid
-
判断openid是否为空
-
openid为空,抛出异常
-
不为空,判断当前用户是否为新用户
-
是新用户,自动完成注册,封装user对象存入数据库并返回
-
不是新用户,查询数据库对应用户并返回
-
-
登录测试成功,返回了数据
{"code":1,"msg":null,"data":{"id":1,"openid":"oK0ST67FECmTNZqeWIKCZ_AomUg4","token":"eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjU3ODgyMzIsInVzZXJJZCI6MX0.QQ8Du4y2IDSPNgocFqMl5imQfWHZDGD4-mm4Z8-eZcs"}}
Day06-18登录令牌校验
1.创建自定义拦截器,即JwtTokenUserInterceptor,获取令牌,检验令牌
继承自HandlerInterceptor
HandlerInterceptor
HandlerInterceptor
是 Spring MVC 框架提供的拦截器接口,用于拦截请求的处理过程。其作用是在请求处理的不同阶段插入自定义的逻辑处理,比如在请求到达 Controller 之前、Controller 处理之后、视图渲染之前等时机执行一些操作。
一般来说,HandlerInterceptor
接口定义了三个方法,分别是 preHandle
、postHandle
和 afterCompletion
。这些方法的作用如下:
-
preHandle
:在请求处理之前调用,可以进行一些前置处理操作,返回值表示是否继续执行后续的处理。 -
postHandle
:在请求处理之后、视图渲染之前调用,可以对请求的处理结果进行进一步的处理。 -
afterCompletion
:在整个请求处理完成后调用,通常用于资源清理等工作。
使用 HandlerInterceptor
的步骤通常包括:
-
创建一个类实现
HandlerInterceptor
接口,实现其中的方法。 -
配置拦截器,可以通过配置类或者 XML 文件进行配置。
-
在配置中指定拦截器的拦截路径,即哪些请求需要被该拦截器处理。
package com.sky.interceptor;
import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌(用户)
String token = request.getHeader(jwtProperties.getUserTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("当前用户id:{}", userId);
// 存储用户id进入ThreadLocal
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
Day06-19C端-接口导入
导入并处理bug
实现接口
-
C端-套餐浏览接口
-
C端-菜品浏览接口
-
C端-分类接口
注意根据套餐id查询包含的菜品列表需要封装DishItemVO
需要从两张表获取数据
1.setmeal_dish 封装name和copies
2.dish image和description
Day07-03缓存菜品代码开发
添加缓存
缓存在Controller层开发
先使用RedisTemplate手动缓存
序列化使用string
在user的DishController里添加缓存
package com.sky.controller.user;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.stream.Collectors;
@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
// TODO 把手动添加的Redis缓存改为使用Spring Cache
// 构造redis中的key,规则:dish_分类id
String key = "dish_" + categoryId;
// 查询缓存中是否存在该分类菜品数据
List<DishVO> list = redisTemplate.opsForList().range(key, 0, -1);
// 如果存在,直接返回
if (list != null && list.size() > 0){
return Result.success(list);
}
// 不存在,则查询数据库,并将数据存入redis
/* 只返回起售的菜品,并把起售的菜品存入缓存 */
list = dishService.listWithFlavor(categoryId);
// 过滤出起售菜品
List<DishVO> availableDishes = list.stream().filter(dishVO -> dishVO.getStatus() == 1)
.collect(Collectors.toList());
if (!availableDishes.isEmpty()){
redisTemplate.opsForList().rightPushAll(key, availableDishes);
}
return Result.success(list);
}
}
清理缓存
😕思考:
-
是否可以在修改方法增加限制,不能修改起售中的菜品,也就是说必须停售才能修
-
新增分类/套餐/菜品的话,默认是未起售状态,对用户端也没有影响,可以不用清理缓存
-
这样新增和修改就不用清除缓存了,只需要在修改状态时清除缓存就可以了
新增菜品、套餐、本来默认就是停售,把分类也停用就可以了
public Integer save(CategoryDTO categoryDTO) {
// 处理不完整的Dto为对象
Category category = new Category();
BeanUtils.copyProperties(categoryDTO, category);
// 默认停用
category.setStatus(0);
return categoryMapper.save(category);
}
代码实现很简单,在Service层的修改方法里加个if()判断就可以了,套餐,分类同理
/**
* 修改菜品及其口味
*
* @param dishDTO
* @return
*/
@Override
@Transactional
public Integer updateWithFlavors(DishDTO dishDTO) {
// 判断菜品是否已经停售否则不能修改
if (dishDTO.getStatus() == StatusConstant.ENABLE) {
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
// 1.封装dish
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
Integer update = dishMapper.update(dish);
// 2.获取dishFlavors
List<DishFlavor> flavors = dishDTO.getFlavors();
Long dishId = dish.getId();
// TODO 修改数据库,让菜品和口味形成多对多关系,才能修改
// 删除原来所有口味,然后插入新口味
dishFlavorMapper.deleteByDishId(dishId);
flavors.stream().forEach(flavor -> {
// 取出每个flavor并修改
flavor.setDishId(dishId);
dishFlavorMapper.insertByDishId(flavor);
});
return update;
}
Day07-11Spring Cache
注解开发缓存,也就是把我之前写的那些代码动态封装到注解里面去了
也就是说现在只需要在user/SetmealController的list方法开启缓存
在admin/SetmealController的changeStatus方法清除缓存就可以了
@GetMapping("/list") @ApiOperation("根据套餐分类id查询套餐") // 框架会动态计算出缓存key,value是这个方法的返回结果 // 根据categoryId来缓存对应套餐分类下的套餐数据 @Cacheable(cacheNames = "setmealCache", key = "#categoryId") public Result<List<Setmeal>> list(Long categoryId) { Setmeal setmeal = new Setmeal(); setmeal.setCategoryId(categoryId); setmeal.setStatus(StatusConstant.ENABLE); List<Setmeal> list = setmealService.list(setmeal); return Result.success(list); }
@PostMapping("/status/{status}") @ApiOperation("套餐起售停售") @CacheEvict(cacheNames = "setmealCache", allEntries = true) public Result changeStatus(@PathVariable Integer status,Long id) { setmealService.changeStatus(id, status); return Result.success(); }
Day07-13添加购物车
🚨小bug:套餐内的菜品没有口味数据
数据库触发器
冗余字段应该是不会经常更改的数据
在规范开发中,数据库通常避免冗余字段,以减少数据一致性问题。然而,在某些情况下,为了优化性能,可能会使用冗余字段。如果主表字段修改了,你需要同时更新相关的冗余字段。常见的方法包括:
-
触发器:在数据库中设置触发器,当主表的字段更新时,触发器会自动更新冗余字段。
-
应用逻辑:在应用层中处理更新逻辑,确保每次主表字段更改时,相关的冗余字段也会被同步更新。
选择合适的方法取决于具体的需求和数据库设计。
根据接口文档开发
-
Controller直接复制
-
service接口直接复制
-
serviceImpl自己写
-
mapper直接复制
-
mapper.xml自己写
吐槽一句
购物车真的粗,只能一个一个添加\减少菜品或者套餐
Day08-02地址簿功能导入
只能有一个地址是默认地址
create table address_book ( id bigint auto_increment comment '主键' primary key, user_id bigint not null comment '用户id', consignee varchar(50) null comment '收货人', sex varchar(2) null comment '性别', phone varchar(11) not null comment '手机号', province_code varchar(12) collate utf8mb4_general_ci null comment '省级区划编号', province_name varchar(32) collate utf8mb4_general_ci null comment '省级名称', city_code varchar(12) collate utf8mb4_general_ci null comment '市级区划编号', city_name varchar(32) collate utf8mb4_general_ci null comment '市级名称', district_code varchar(12) collate utf8mb4_general_ci null comment '区级区划编号', district_name varchar(32) collate utf8mb4_general_ci null comment '区级名称', detail varchar(200) collate utf8mb4_general_ci null comment '详细地址', label varchar(100) collate utf8mb4_general_ci null comment '标签', is_default tinyint(1) default 0 not null comment '默认 0 否 1是' )
功能分析与设计
本质都是单表操作
-
增删改查
-
设置默认地址
-
查询默认地址(订单页面)
Day08-04地址簿代码开发
没懂为什么要把其他地址设置为0,不能精确查找吗
@Override @Transactional public void setDefault(AddressBook addressBook) { //1、将当前用户的所有地址修改为非默认地址 update address_book set is_default = ? where user_id = ? addressBook.setIsDefault(0); addressBook.setUserId(BaseContext.getCurrentId()); addressBookMapper.updateIsDefaultByUserId(addressBook); //2、将当前地址改为默认地址 update address_book set is_default = ? where id = ? addressBook.setIsDefault(1); addressBookMapper.update(addressBook); }
Day08-05订单功能
📜tips:
这个不难,难得是优惠卷和秒杀价还有满减的逻辑
提交订单接口开发分析
数据库层处理
-
根据Id查询地址簿表
-
根据userId查询购物车列表
-
订单表插入一条数据,并且要返回id
-
订单详情表根据订单id插入n条数据
-
根据userId删除购物车表一条数据
业务逻辑层处理
-
处理业务异常
-
地址簿是否为空
-
购物车为空
-
-
对Order表插入1条数据
-
通过Dto构造Order,拷贝后设置其余属性
-
-
对Order_Detail表插入n条数据
-
购物车和订单详细的表结构很类似,可以直接拷贝
-
-
清空用户购物车数据
-
返回VO返回结果
请求数据分析
{ "addressBookId": 0, "amount": 0, "deliveryStatus": 0, "estimatedDeliveryTime": "yyyy-MM-dd HH:mm:ss", "packAmount": 0, "payMethod": 0, "remark": "string", "tablewareNumber": 0, "tablewareStatus": 0 }
很明显是OrderSubmitDTO
响应数据分析
{ "code": 0, "data": { • "id": 0, • "orderAmount": 0, • "orderNumber": "string", • "orderTime": "2019-08-24T14:15:22Z" }, "msg": "string" }
很明显是OrderSubmitVO
后端重新校验金额
金额为什么不需要重新验证呢
我觉得金额需要后端重新验证
金额是前端传过来,非常的不安全
但是还有打包费和配送费,配送费在订单数据库内没有
Day08-12微信支付
什么内网穿透,什么配置啥的全搞好了告诉我个人不能支付
😠凭什么个人用户不能用小程序支付
这里只能直接跳过支付
需要创建一个全局变量
private Orders orders;
然后在用户下单保存之前赋值
this.orders = orders;
然后在订单支付里改变订单的状态
/**
* 订单支付
*
* @param ordersPaymentDTO
* @return
*/
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("该订单已支付");
}
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));*/
// 睡眠3秒,模拟支付过程
Thread.sleep(3000);
//模拟支付,构造一个模拟的支付成功订单号
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; //订单状态,待接单
LocalDateTime check_out_time = LocalDateTime.now();//更新支付时间
orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time, this.orders.getId());
// 模拟支付,提示订单支付成功
Map map = new HashMap();
map.put("type",1);
map.put("orderId",this.orders.getId());
map.put("content","订单号:"+this.orders.getNumber());
String jsonString = JSON.toJSONString(map);
webSocketServer.sendToAllClient(jsonString);
return vo;
}
在OrderMapper中写入如下代码:
@Update("update orders set status = #{orderStatus},pay_status = #{orderPaidStatus} ,checkout_time = #{check_out_time} where id = #{id}") void updateStatus(Integer orderStatus, Integer orderPaidStatus, LocalDateTime check_out_time, Long id);
bug1
前端传来的DTO配送状态始终为0,且没有预计送达时间
bug2
餐具选择状态均为0选择具体数量,即没有1按餐量提供
Day09用户端订单接口
1.查询历史订单
请求地址/user/order/historyOrders
需求分析与设计
业务规则
-
分页查询历史订单
-
可以根据订单状态查询
-
展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片)
请求参数分析
本来以为传的是OrdersPageQueryDTO
结果传的三个string 心态💥了
响应结果分析
多表分页查询,并且返回数据肯定要封装为OrderVO对象,封装为PageResult<OrderVO>
这里太不合理了,应该让前端返回dto的,还让我自己在后端返回dto
/** * 订单分页查询 * @param page * @param pageSize * @param status * @return */ @Override public PageResult orderPageQuery(int page, int pageSize, Integer status) { // 1.获取页数和页面大小 PageHelper.startPage(page, pageSize); // 2.获取userId Long userId = BaseContext.getCurrentId(); // 3.封装Dto为分页查询准备参数 OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO(); ordersPageQueryDTO.setUserId(userId); ordersPageQueryDTO.setStatus(status); // 分页条件查询 Page<Orders> pageOrder = orderMapper.pageQuery(ordersPageQueryDTO); // 创建一个空的VO列表 List<OrderVO> list = new ArrayList(); // 4.为每一个订单查询订单详情并封装到OrderVO中,使用foreach实现 if (pageOrder != null && pageOrder.getTotal() > 0) { if (pageOrder == null || pageOrder.getTotal()>0){ pageOrder.stream().forEach(orders ->{ Long id = orders.getId(); List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id); OrderVO orderVO = new OrderVO(); BeanUtils.copyProperties(orders,orderVO); orderVO.setOrderDetailList(orderDetailList); list.add(orderVO); } ); } } return new PageResult(pageOrder.getTotal(), list); }
2.查询订单详情
/user/order/orderDetail/{id}
多表查询,把查询结果封装为VO对象返回
3.取消订单
业务规则:
-
待支付和待接单状态下,用户可直接取消订单
-
商家已接单状态下,用户取消订单需电话沟通商家(不关我事)
-
派送中状态下,用户取消订单需电话沟通商家(不关我事)
-
如果在待接单状态下取消订单,需要给用户退款(不关我事)
-
取消订单后需要将订单状态修改为“已取消”
实际上只有两个业务
4.再来一单
本质是就是把传过来的订单数据查出来,然后新增对应购物车
Day09商家端订单管理模块
订单搜索
本质:订单VO的分页查询
多表查询:订单表,订单详情表
请求参数分析:OrderQueryPageDto
响应结果分析:Result<PageResult>,PageResult内部应该是List<OrderVO>,并且后端需要VO属性 Orderdishes(订单菜品信息)
订单状态数量统计
疑惑:为什么只统计待接单、待派送、派送中的订单数量,已完成的呢?
数据库本质:多次单表查询
业务规则:直接查
请求参数分析:无参数,自己写状态12345
响应结果分析:OrderStatisticsVO,
-
@Data @Builder public class OrderStatisticsVO implements Serializable { //待接单数量 private Integer toBeConfirmed; //待派送数量 private Integer confirmed; //派送中数量 private Integer deliveryInProgress; }
优化思路:封装为一次批量查询所有状态数量,直接得到list,再到业务层处理成前端要的
测试:成功显示
查询订单详情
数据库本质:
-
根据id查询OrderDetail
-
多次单表查询
业务规则:
-
订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)
-
订单详情页面需要展示订单明细数据(商品名称、数量、单价)
请求参数分析:Order的id
响应结果分析:OrderVO
📜tips:写用户端的订单功能时已经写好,直接复用
测试:Controller参数忘记加@PathVariable
收不到id,路径参数一定要加,加了就好了
接受订单
数据库本质:
-
修改订单状态
-
单表操作
业务规则:
-
商家接单其实就是将订单的状态修改为“已接单”
请求参数分析:id和status(不是),Apifox显示未json,就是RequestBody,请求头参数,就是Dto啊
吐槽:
-
我真是吐了,改个单表status还传Dto
-
或许是为了严谨,真正的修改订单状态可能涉及多张表的改变
响应请求分析:修改操作无返回
📜tips:Mapper的Update写成动态,方便复用
测试:前端传递的参数为空
写到拒单的时候我懂了,不是不传状态,而是在后台写死了就是变成已接单(3),
思考之后我认为后端写死是合理的,因为前端传来的状态是不可靠的,可能被篡改
拒绝订单
数据库本质:修改订单表,单表操作
业务规则:
-
商家拒单其实就是将订单状态修改为“已取消”
-
只有订单处于“待接单”状态时可以执行拒单操作
-
商家拒单时需要指定拒单原因
-
商家拒单时,如果用户已经完成了支付,需要为用户退款
请求参数分析:application/json 即 dto,需要@RequestBody
响应结果分析:修改操作无返回结果
取消订单
数据库本质:修改订单表,单表操作
业务规则:
-
取消订单其实就是将订单状态修改为“已取消”
-
商家取消订单时需要指定取消原因
-
商家取消订单时,如果用户已经完成了支付,需要为用户退款
请求参数分析:application/json
,OrdersCancelDTO
响应结果分析:无
😓吐槽:这不和拒绝一样吗,还用的着专门写个dto吗,隔离隔离,解耦解耦
优化:不允许取消已经完成的订单,并且自定义原因长度不能小于5
并且我觉得用户和商家取消得区分一下
测试:成功
派送订单
数据库本质:修改订单表,单表操作
业务规则:
-
派送订单其实就是将订单状态修改为“派送中”
-
只有状态为“待派送”的订单可以执行派送订单操作
请求参数分析:/admin/order/delivery/{id} string 地址栏参数 @PathVariable
响应结果分析:无
测试:成功
完成订单
数据库本质:修改订单表,单表操作
业务规则:
-
完成订单其实就是将订单状态修改为“已完成”
-
只有状态为“派送中”的订单可以执行订单完成操作
请求参数分析:/admin/order/complete/{id} String 地址栏参数 @PathVariable
响应结果分析:无
测试:成功
校验收货地址是否超出配送范围
配置
shop: address: your_address baidu: ak: your_ak
Service直接读配置文件
@Value("${sky.shop.address}") private String shopAddress; @Value("${sky.baidu.ak}") private String ak;
测试:我累个骚刚,一万九千多米
Day10Spring Task
启动类开启注解@EnableScheduling
创建自定义任务类,需要加注解@Scheduled
像美团这种应该还有个骑手端,由骑手来确认派送完成(或者后台自动判断?)
本来想在员工里设置几个骑手,结果员工根本没有分权限,我不知道搞这个员工表干嘛
/** * 处理前一天仍然处于“派送中”状态的订单 * 我觉得真是业务应该不会这么处理 */ @Scheduled(cron = "0 0 1 * * ?") public void processDeliveryOrder(){ // select * from orders where status = 4 and order_time < 当前时间-1小时 LocalDateTime time = LocalDateTime.now().plusMinutes(-60); List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time); if(ordersList != null && ordersList.size() > 0){ ordersList.forEach(order -> { order.setStatus(Orders.COMPLETED); orderMapper.update(order); }); } }
tis:这个超时未支付订单应该用消息队列来做
我觉得这些功能消息队列都能做,但是我该没学
WebSocket
WebSocket是基于TCP的一种新的网络协议。它实现了浏览器域服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的链接,并进行双向数据传输。
HTTP是短连接,是单向的,基于请求响应模式;WebSocket是长连接(有点像打电话,双向消息),支持双向通信。HTTP和WebSocket底层都是TCP连接。
应用:视频弹幕,网页聊天(聊天窗口和客服聊天),体育实况更新,股票基金报价实时更新。
入门案例开发步骤
①直接使用websocket.html页面作为WebSocket客户端
②导入WebSocket的maven坐标(已导入)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ③导入WebSocket服务端组件WebSocketServer,用于和客户端通信
④导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
⑤导入定时任务类WebSocketTask,定时向客户端推送数据
定时处理未支付订单
/** * 处理待付款(状态1)超时订单 * 待付款订单(状态1)超过15分钟未支付,则自动取消 * 模拟支付里不存在这种订单,直接去数据库改一下 */ @Scheduled(cron = "0 * * * * ?")// 每分钟执行一次 public void processTimeoutOrder() { log.info("处理支付超时订单:{}", new Date()); LocalDateTime time = LocalDateTime.now().plusMinutes(-15); // 查询超时待付款订单 List<Orders> orders = orderMapper.getByStatusAndOrderTime(Orders.PENDING_PAYMENT, time); // 对超时订单进行状态更新 if (orders != null && orders.size() > 0) { orders.forEach(order -> { order.setStatus(Orders.CANCELLED); order.setCancelReason("订单超时,自动取消"); order.setCancelTime(LocalDateTime.now()); orderMapper.update(order); }); } // 批量修改,性能更高 // orderMapper.updateBatch(orders); }
批量修改失败,xml问题
来单提醒
数据库操作:无
业务规则:
客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报。
约定服务期发送给客户端浏览器的数据格式位JSON,字段包括:type(消息类型,1来单提醒,2客户催单),orderId,content。
具体操作
-
在支付成功后调用webSocket
-
封装map信息,转化为json
//通过websocket向客户端浏览器推送消息 type orderId content Map map = new HashMap(); map.put("type",1); map.put("orderId",this.orders.getId()); map.put("content","订单号:"+this.orders.getNumber()); String json = JSON.toJSONString(map); webSocketServer.sendToAllClient(json);
测试成功
客户催单
请求路径:/user/order/reminder/{id}
业务分析:
-
通过WebSocket实现管理端页面和服务端保持长连接状态
-
当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息
-
客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
-
约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderld,content
-
type 为消息类型,1为来单提醒 2为客户催单
-
orderld 为订单id
-
content 为消息内容
-
请求参数分析:路径参数,订单id
响应结尾分析:无
测试:成功