微信小程序隶属于前端,因此我们只需要了解掌握一些基本的功能与业务逻辑即可。
HttpClient
HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
核心AP1:
HttpClient(接口)
HttpClients
CloseableHttpClient(HttpClient的实现类)
HttpGet
HttpPost
发送请求步骤:
- 创建HttpClient对象
- 创建Http请求对象
- 调用HttpClient的execute方法发送请求
导入坐标:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
测试Get请求
package com.sky.test;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest
public class HttpClientTest {
@Test
public void httpget() throws IOException {
//创建httpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost/api/category/page?page=1&pageSize=10");
//发送请求,获得响应结果
CloseableHttpResponse response = httpClient.execute(httpGet);
//解析响应结果
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("状态码为:"+statusCode);
HttpEntity entity = response.getEntity();
String string = EntityUtils.toString(entity);
System.out.println("返回数据为:"+string);
//关闭资源
response.close();
httpClient.close();
}
}
可以看到其状态码为401,并且获取不到数据,按照我们之前的定义,这是由于JWT令牌认证过期导致的,因此我们可以登录一下从而获取JWT令牌。
我们重新登录后发现还是有问题,依旧是401,没办法,只能把注册拦截器部分的代码先注释掉了,注释后就可以获取到数据了。
测试Post请求
@Test
public void testPost() throws JSONException, IOException {
//创建httpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建Post请求对象
HttpPost httpPost = new HttpPost("http://localhost/api/employee/login");
//登录接收的是JSON对象,因此创建一个JSON对象并封装
JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");
StringEntity entity = new StringEntity(jsonObject.toString());
//指定请求编码方式
entity.setContentEncoding("utf-8");
//指定数据格式
entity.setContentType("application/json");
httpPost.setEntity(entity);
CloseableHttpResponse response = httpClient.execute(httpPost);
//解析响应结果
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("状态码为:"+statusCode);
HttpEntity entity1 = response.getEntity();
String string = EntityUtils.toString(entity1);
System.out.println("返回数据为:"+string);
//关闭资源
response.close();
httpClient.close();
}
拦截器并不会拦截登录请求,所以将拦截器注册代码恢复即可。
微信小程序基础语法
这里我们主要是掌握微信小程序的一些最基本的语法,知道其具体所代表的含义即可。
从语法上来看,小程序与前端还是很相近的。
微信小程序开发
- 注册:在微信公众平台注册小程序,完成注册后可以同步进行信息完善和开发。
- 小程序信息完善:填写小程序基本信息,包括名称、头像、介绍及服务范围等
- 开发小程序:完成小程序开发者绑定、开发信息配置后,开发者可下载开发者工具、参考开发文档进行小程序的开发和调试。
- 提交审核和发布:完成小程序开发后,提交代码至微信团队审核,审核通过后即可发布(公测期间不能发布)
选上不校验合法域名,保证可以正常发送请求
小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成,必须放在项目的根目录,即app.js
,app.json
以及app.wxss
。
登录功能
微信小程序的登录功能流程如下图所示:
- 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
- 调用 auth.code2Session 接口,换取 用户唯一标识
OpenID
、
用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key。 - 之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
具体步骤如下:
调用wx.login
获得:code:0c3JRx000EEYWR1NO0100ifYnK2JRx0U
向微信服务器请求唯一id的地址是:
https://api.weixin.qq.com/sns/jscode2session
我们使用Postman来请求,将上面的地址写入后,需要传入下面4个参数:
将上面的四个参数写入postman后发送请求获得唯一id,得到的openid就是微信用户的唯一标识,这个值是不变的。
这个请求只能使用一次,因为code是变化的,如果再次请求就会得到下面的报错信息:
用户登录小程序功能开发
这里只是小程序登录功能的逻辑代码,我们并不是专门的小程序开发人员,掌握如何请求即可。
用户登录后端功能开发
了解了微信小程序登录的过程后,我们就可以进行用户登录功能的实现了。
用户登录的业务逻辑如下:
- 发送请求给微信服务端,获得唯一标识Openid
- 判断Openid在数据库中是否存在,如果没有,则将该用户注册
微信小程序发送登录请求,调用wx.login()
方法,这个是微信为我们提供的。
wx.login
获得的结果res中包含我们所需要的code
。
wxlogin(){
var that=this;
wx.login({
success: (res) => {
// console.log(res.code)
wx.request({
url: 'http://localhost/user/user/login',
method:"POST",
data:{"code":res.code},
success:function(res){
console.log(res.data.data.token)
that.setData({
token:res.data.data.token
})
}
})
},
})
},
Controller
层开发,请求接口是user/user/login
,发送的请求参数为JSON封装的实体类UserLoginDTO 类型,该实体类的属性只有一个,就是我们先前提到的code,该码是由小程序端调用wx.login
获取的,该码每次请求都不相同。
package com.sky.dto;
import lombok.Data;
import java.io.Serializable;
/**
* C端用户登录
*/
@Data
public class UserLoginDTO implements Serializable {
private String code;
}
获得请求后,调用wxlogin
的服务层方法,进行与微信服务端的交互,随后将获取到的用户信息生成JWT
令牌,并将获取的用户信息返回。
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("微信用户登录:{}",userLoginDTO.getCode());
// 微信登录
User user = userService.wxLogin(userLoginDTO);
// 为微信用户生成jwt令牌
HashMap<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID, user.getId());
String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);
UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.openid(user.getOpenid())
.token(token)
.build();
return Result.success(userLoginVO);
}
wxlogin
的服务层代码如下:
其首先调用微信接口服务,利用传入的code
获得微信用户的Openid
,获取用户Openid
的请求方法定义如下,在这里面封装定义了微信登录所需要的四个参数以及请求地址,并从返回结果中提取了Openid
public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";
private String getOpenid(String code) {
//调用微信接口服务,获得当前微信用户的openid
Map<String, String> map = new HashMap<>();
map.put("appid",weChatProperties.getAppid());
map.put("secret",weChatProperties.getSecret());
map.put("js_code",code);
map.put("grant_type","authorization_code");
String json = HttpClientUtil.doGet(WX_LOGIN, map);
JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");
return openid;
}
获得Openid
后,执行userMapper.getByOpenId(openid);
判断用户是否存在,如果是新用户,则进行注册,注册时需要填入的信息只有Openid
和注册时间,并将这个用户对象返回。
@Override
public User wxLogin(UserLoginDTO userLoginDTO) {
// 调用微信接口服务,获取当前微信用户的Openid
String openid = getOpenid(userLoginDTO.getCode());
// 判断openId是否为空,如果为空标识登录失败,抛出业务异常
if (openid == null) {
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}
// 判断当前用户是否为新用户
User user = userMapper.getByOpenId(openid);
// 如果是新用户,自动完成注册
if (user == null) {
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now()).build();
userMapper.insert(user);
}
// 返回这个用户对象
return user;
}
注册用户的Mybatis
对应的SQL代码:
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into user(openid, name, phone, sex, id_number, avatar, create_time)
VALUES (#{openid},#{name},#{phone},#{sex},#{idNumber},#{avatar},#{createTime})
</insert>
随后我们进行登录:
菜品缓存
为什么要使用菜品缓存呢,因为每次查询数据库,尤其是当多人访问时,会存在系统响应慢,用户体验差的问题,而利用Redis来缓存数据,减少数据库查询操作,可以提升用户体验。
缓存逻辑
- 每一份缓存是一个类别下的数据。
- 当数据发生变更时,要及时清理缓存数据。
代码如下:
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
// 构造redis中的key,规则:dish_分类Id
String key = "dish_" + categoryId;
// 查询redis中是否存在菜品数据
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
if (list != null && list.size() > 0) {
// 如果存在,直接返回,无需查询数据库
return Result.success(list);
}
// 如果不存在,查询数据库,将查询到的数据放入redis中
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
list = dishService.listWithFlavor(dish);
// 放入redis
redisTemplate.opsForValue().set(key, list);
return Result.success(list);
}
那么当管理端进行了菜品修改,删除,起售停售,新增菜品时,菜品信息都发生了变化,此时我们就需要清理缓存,不同的是新增我们可以精确清理,而其他的几种情况我们采用全部清除的方式。
但我们依旧可以通过一个方法来实现:
private void clearCache(String pattern) {
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
Spring Cache缓存框架
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,如:
- EHCache
- Caffeine
- Redis
此处我们的缓存实现自然是Redis。当然如果我们后期想要换成其他的缓存实现,如EHCache,我们只需要导入EHCache的坐标即可,因为其注解是通用的,因此代码无需变化。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能,通常加在启动类上 |
@Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除 |
这些注解如何使用呢,我们使用一个简单的增删改查来使用一下:
Spring Cache
为Contoller
创建代理对象,有代理对象来实现这些注解中的方法,通过代理对象来操作Redis,即操作Redis的是代理对象,对于缓存中没有的情况,如@Cacheable
注解时,会通过反射来执行下面的方法。
注解中的key是动态求出的。
package com.itheima.controller;
import com.itheima.entity.User;
import com.itheima.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserMapper userMapper;
/**
* 如果使用Spring Cache 缓存数据,key的生成:userCache::1
* @param user #result.id是一种Spring表达式语言
* @return
*/
@PostMapping
@CachePut(cacheNames = "userCache",key = "#user.id")//参数取到的
// @CachePut(cacheNames = "userCache",key = "#result.id")结果中取得的,这个result是固定的
// @CachePut(cacheNames = "userCache",key = "#p0.id")//p,a,root.args打头,0代表第一个参数
// @CachePut(cacheNames = "userCache",key = "#a0.id")
// @CachePut(cacheNames = "userCache",key = "#root.args[0].id")
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
@DeleteMapping
@CacheEvict(cacheNames = "userCache",key = "#id") //删除某个key对应的缓存数据
public void deleteById(Long id){
userMapper.deleteById(id);
}
@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache",allEntries = true) //删除userCache下所有的缓存数据,不再需要key了
public void deleteAll(){
userMapper.deleteAll();
}
@GetMapping
@Cacheable(cacheNames = "userCache",key = "#id")//若在缓存中查到就不会执行get方法,否则通过反射调用这个方法,这是由于Spring Cache是基于代理实现的,其会实现Controller的代理对象去执行
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}
}
小知识:Redis
的:
会使数据存储呈现树形结构
Spring Cache缓存套餐
具体的实现思路如下:
- 导入
Spring Cache
和Redis
相关maven
坐标 - 在启动类上加入
@EnableCaching
注解,开启缓存注解功能 - 在用户端接口
SetmealControler
的list
方法上加入@Cacheable
注解 - 在管理端接口
SetmealController
的save
、delete
、update
、startOrStop
等方法上加入CacheEvict注解
在用户端的查询缓存设置如下:
@GetMapping("/list")
@ApiOperation("根据分类id查询套餐")
@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);
}
用户下单
订单支付
在下单成功后,要想支付需要开通支付账号,但我们并不是商家,无法具备相应资质,因此我们在本部分只需要了解微信支付的流程,等具体开发时,将对应的配置文件写好即可。
微信支付时序图
重点步骤说明:
- 步骤3 用户下单发起支付,商户可通过
JSAPI
下单创建支付订单。生成步骤4中的预支付订单 - 步骤8 商户可在微信浏览器内通过
JSAPI
调起支付API
调起微信支付,发起支付请求。 - 步骤15 用户支付成功后,商户可接收到微信支付支付结果通知支付结果通知
API
。 - 步骤20 商户在没有接收到微信支付结果通知的情况下需要主动调用查询订单
API
查询支付结果。(修改我们的系统的数据)
微信支付准备工作
在微信支付时序图中我们看到,商家系统要将订单数据发送到微信服务端(步骤3)
,那么在数据传输过程中该如何保证数据安全呢?这就涉及到一些加密
,签名
等操作了。
那么具体该如何做呢?
首先获取微信平台证书,商户私钥文件
此外,在订单完成后,服务器会向商家系统返回支付结果,事实上就是微信服务端发送一个http请求给商家系统,这就要求微信服务器能够找到我们的公网地址(步骤21)
,而我们的项目此时部署在本地,是一种局域网,那么微信服务器该如何找到呢?这就涉及到内网穿透
技术了。
这里要实现内网穿透,我们需要借助一个工具cpolar
,它可以帮我们临时生成一个公网地址用于访问。
下载这个工具并安装:
随后我们配置隧道,将下面的Token复制下来后在执行命令:
cpolar.exe authtoken 你的Token
只需要执行一次就可以了。随后进行地址映射,继续输入:
cpolar.exe http 8080
生成一个临时的公网域名
随后我们就可以使用这个公网地址来访问我们的项目了。
当然,由于这个项目是一个前后端项目,通过nginx
的方式进行反向代理,按照其配置文件,可以发现,监听的是80端口
因此要想访问项目,应该开放的是80端口
这个回调地址中的王子就是我们内网穿透的地址。
订单状态定时处理
这里面主要涉及两种,一种是下单后要求15分钟内付款,否则取消订单,另一种则是派送中的订单一定时间后自动完成。
那么如何实现上面的定时任务呢?可以用Spring
提供的工具SpringTask
Spring Task 是spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
Cron表达式规则
cron
表达式其实就是一个字符串,通过cron
表达式可以定义任务触发的时间构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
这个日期的表达相对简单,那么对一些复杂日期,如描述二月的最后一天,那么这个日期是28还是29呢,这就涉及到较为复杂的Cron
表达式的编写了,当然我们也不用担心,因为我们可以使用Cron
的在线生成器来实现。
https://cron.ciding.cc/
Spring Task入门案例
Spring Task使用步骤
- 导入maven坐标 spring-context(已存在,该包很小,包含在spring-context中)
- 启动类添加注解 @EnableScheduling开启任务调度
- 自定义定时任务类,要使用
@Scheduled
注解
@Component//需要实例化,交给Spring容器去管理
@Slf4j
public class MyTask {
/**
* 定时任务 每隔5秒触发一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask(){//定时任务的处理逻辑方法
log.info("定时任务开始执行:{}",new Date());
}
}
订单状态定时处理
用户下单后可能存在的情况:
- 下单后未支付,订单一直处于“待支付”状态
- 用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态
对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:
- 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为
“已取消
” - 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“
已完成
”
/**
* 处理支付超时订单
*/
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder() {
log.info("处理支付超时订单:{}", new Date());
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTime(Orders.PENDING_PAYMENT, time);
if (ordersList != null && ordersList.size() > 0) {
ordersList.forEach(orders -> {
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("支付超时,自动取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
});
}
}
@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrder(){
log.info("处理派送中订单:{}", new Date());
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTime(Orders.DELIVERY_IN_PROGRESS, time);
if (ordersList != null && ordersList.size() > 0) {
ordersList.forEach(orders -> {
orders.setStatus(Orders.COMPLETED);
orderMapper.update(orders);
});
}
}
<select id="getByStatusAndOrderTime"
resultType="com.sky.entity.Orders">
select *
from orders where status=#{status} and order_time<#{orderTime};
</select>
WebSocket
WebSocket基础概念
WebSocket
是基于 TCP
的一种新的网络协议。它实现了浏览器与服务器全双工通信–浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
应用场景:
视频弹幕,网页聊天,体育实况更新,股票基金报价实时更新
我们可以看到,以前发送的请求都是http
打头的,现在却是ws
消息在Messages
中展示:
WebSocket入门案例
实现步骤
- 直接使用websocket.html页面作为WebSocket客户端
- 导入WebSocket的maven坐标
- 导入WebSocket服务端组件WebSocketServer,用于和客户端通信
- 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
- 导入定时任务类WebSocketTask,定时向客户端推送数据
1.前端html如下,里面涉及了大量的回调方法,回调方法就是这些指定的方法运行了,便会执行这些方法,类似于监听方法,但又不同,举一个简单例子:
你周五放学回家,你问你老妈煮好饭没,你妈说还没煮;然后你跟她说: 老妈,我看下喜羊羊,你煮好饭叫我哈!
分析:你和老妈约定了一个接口,你通过这个接口叫老妈煮饭,当饭煮好了的时候,你老妈 又通过这个接口来反馈你,“饭煮好了”!
<!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.加入Maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
3.导入WebSocket服务端组件WebSocketServer,用于和客户端通信
这个WebSocketServer的作用其实与Controller相同,就是接收请求,只不过是ws请求,里面用到了很多注解,其中有些注解声明为回调函数
package com.sky.websocket;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}")//就是接收html中的//ws://localhost:8080/ws/+clientId
public class WebSocketServer {
//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();
//这个session是WS中的session
/**
* 连接建立成功调用的方法
*/
@OnOpen//这就是回调函数,执行完连接后会执行下面的方法
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
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的服务端组件
package com.sky.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
导入定时任务类WebSocketTask,定时向客户端推送数据
package com.sky.task;
import com.sky.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@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()));
}
}
最终,整个执行流程是这样的,当我们将服务启动时,配置文件中生成的WebSocketServer
对象便会加入到Spring容器中了,此时当我们打开html页面后,便会发送ws://localhost:8080/ws/+clientId
,此时服务端根据配置的@ServerEndpoint(“/ws/{sid}”)注解便能够接收到这个请求,从而执行建立会话方法(通过@OnOpen
注解),随后定时任务 WebSocketTask
会每5秒钟向客户端发送请求,同时当客户端发送消息时会调用服务端的回调函数(通过@OnMessage
注解)。
来单提醒与催单
- 通过WebSocket实现管理端页面和服务端保持长连接状态
- 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
- 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
- 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderld,content
当我们打开前端也面后,js
代码执行WS
请求,这里需要注意的时发起请求的端口是80,这是由于nginx
反向代理转发实现的。
随后服务器端也接收到了请求并创建了连接
随后,在订单支付成功的业务中调用webSocketServer.sendToAllClient(JSON.toJSONString(map));
来向客户端发送消息
public void paySuccess(String outTradeNo) {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
// 根据订单号查询当前用户的订单
Orders orderDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);
// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(orderDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
HashMap map = new HashMap();
map.put("type", 1);
map.put("orderId", orders.getId());
map.put("content", "订单号:" + outTradeNo);
// 通过WebSocket实现来电提醒,向客户端浏览器推送消息
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}
向客服端发送消息的方法
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
而客户端接收到消息后便会提示。需要注意的是这里有个bug,如果是在表单中直接点击接单或取消还会让消息一直响。
微信小程序模拟下单
由于我们没有商家资质,因此无法实现真正的微信支付,这里我们可以想办法跳过去,具体该怎么做呢,首先我们先将整个下单过程的流程梳理一下:
- List item
代码运行时的问题
报错1:地址解析失败
下单时点击没有反应,提示地址解析失败
,这个是由于调用了百度地图接口,会判断当前派送地址是否超出派送范围,我们将这个判断注销即可:
即下面的代码:
checkOutOfRange(addressBook.getCityName() + addressBook.getDistrictName() + addressBook.getDetail());
整个下单服务逻辑代码如下:
@Override
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
// 异常情况的处理(收货地址为空、超出配送氛围、购物车为空)
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if (addressBook == null) {
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
// 检查用户的收货地址是否超出配送范围
//checkOutOfRange(addressBook.getCityName() + addressBook.getDistrictName() + addressBook.getDetail());
Long currentId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
// 只能查询当前用户数据
shoppingCart.setUserId(currentId);
// 查询当前用户的购物车数据
List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
if (shoppingCartList == null || shoppingCartList.size() == 0) {
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
//构造订单数据
Orders order = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO,order);
order.setPhone(addressBook.getPhone());
order.setAddress(addressBook.getDetail());
order.setConsignee(addressBook.getConsignee());
order.setNumber(String.valueOf(System.currentTimeMillis()));
order.setUserId(currentId);
order.setStatus(Orders.PENDING_PAYMENT);
order.setPayStatus(Orders.UN_PAID);
order.setOrderTime(LocalDateTime.now());
orderMapper.insert(order);
// 订单明细数据
ArrayList<OrderDetail> orderDetailList = new ArrayList<>();
shoppingCartList.forEach(cart->{
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(cart, orderDetail);
orderDetail.setOrderId(order.getId());
orderDetailList.add(orderDetail);
});
// 向明细表中查询n条数据
orderDetailMapper.insertBatch(orderDetailList);
// 清理购物车中的数据
shoppingCartMapper.deleteByUserId(currentId);
// 封装返回结果
OrderSubmitVO submitVO = OrderSubmitVO.builder()
.id(order.getId())
.orderNumber(order.getNumber())
.orderAmount(order.getAmount())
.orderTime(order.getOrderTime())
.build();
return submitVO;
}
报错2:订单查询失败
这个是软件的一个小Bug,因为小程序中发送的参数名与后端接口接收的参数名不对应,导致500
的错误,只需要让小程序中的参数与后端参数相同即可,我们可以任选一端进行修改。
小程序与后端的代码如下,原本错误是由于小程序中是page
,而后端是pageNum
,两个改为一致即可。
@GetMapping("historyOrders")
@ApiOperation("历史订单查询")
public Result<PageResult> page(Integer pageNum, Integer pageSize, Integer status) {
PageResult pageResult=orderService.pageQueryForUser(pageNum, pageSize, status);
return Result.success(pageResult);
}
pageInfo: {
pageNum: 1,
pageSize: 10,
total: 0 },
var params = {
pageSize: 10,
pageNum: this.pageInfo.pageNum,
status: this.status !== '' ? this.status : '' };