微信小程序开发

微信小程序隶属于前端,因此我们只需要了解掌握一些基本的功能与业务逻辑即可。

HttpClient

HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。

核心AP1:
HttpClient(接口)
HttpClients
CloseableHttpClient(HttpClient的实现类)
HttpGet
HttpPost

发送请求步骤:

  1. 创建HttpClient对象
  2. 创建Http请求对象
  3. 调用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 {@Testpublic  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请求

@Testpublic 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();}

拦截器并不会拦截登录请求,所以将拦截器注册代码恢复即可。

在这里插入图片描述

微信小程序基础语法

这里我们主要是掌握微信小程序的一些最基本的语法,知道其具体所代表的含义即可。
从语法上来看,小程序与前端还是很相近的。

微信小程序开发

  1. 注册:在微信公众平台注册小程序,完成注册后可以同步进行信息完善和开发。
  2. 小程序信息完善:填写小程序基本信息,包括名称、头像、介绍及服务范围等
  3. 开发小程序:完成小程序开发者绑定、开发信息配置后,开发者可下载开发者工具、参考开发文档进行小程序的开发和调试。
  4. 提交审核和发布:完成小程序开发后,提交代码至微信团队审核,审核通过后即可发布(公测期间不能发布)

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

选上不校验合法域名,保证可以正常发送请求

在这里插入图片描述

小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成,必须放在项目的根目录,即app.jsapp.json以及app.wxss

在这里插入图片描述

登录功能

微信小程序的登录功能流程如下图所示:

wx0d18cfe84917988b

  1. 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
  2. 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID
    用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key。
  3. 之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

具体步骤如下:

调用wx.login获得:code:0c3JRx000EEYWR1NO0100ifYnK2JRx0U
向微信服务器请求唯一id的地址是:
https://api.weixin.qq.com/sns/jscode2session
我们使用Postman来请求,将上面的地址写入后,需要传入下面4个参数:

在这里插入图片描述
将上面的四个参数写入postman后发送请求获得唯一id,得到的openid就是微信用户的唯一标识,这个值是不变的。

在这里插入图片描述

这个请求只能使用一次,因为code是变化的,如果再次请求就会得到下面的报错信息:

在这里插入图片描述

用户登录小程序功能开发

这里只是小程序登录功能的逻辑代码,我们并不是专门的小程序开发人员,掌握如何请求即可。

用户登录后端功能开发

了解了微信小程序登录的过程后,我们就可以进行用户登录功能的实现了。
用户登录的业务逻辑如下:

  1. 发送请求给微信服务端,获得唯一标识Openid
  2. 判断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) {//调用微信接口服务,获得当前微信用户的openidMap<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和注册时间,并将这个用户对象返回。

	@Overridepublic User wxLogin(UserLoginDTO userLoginDTO) {
//        调用微信接口服务,获取当前微信用户的OpenidString 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来缓存数据,减少数据库查询操作,可以提升用户体验。

在这里插入图片描述
缓存逻辑

  1. 每一份缓存是一个类别下的数据。
  2. 当数据发生变更时,要及时清理缓存数据。

代码如下:

	@GetMapping("/list")@ApiOperation("根据分类id查询菜品")public Result<List<DishVO>> list(Long categoryId) {
//        构造redis中的key,规则:dish_分类IdString 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);//        放入redisredisTemplate.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 提供了一层抽象,底层可以切换不同的缓存实现,如:

  1. EHCache
  2. Caffeine
  3. Redis

此处我们的缓存实现自然是Redis。当然如果我们后期想要换成其他的缓存实现,如EHCache,我们只需要导入EHCache的坐标即可,因为其注解是通用的,因此代码无需变化。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>
注解说明
@EnableCaching开启缓存注解功能,通常加在启动类上
@Cacheable在方法执行前先查询缓存中是否有数据,如果有数据则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut将方法的返回值放到缓存中
@CacheEvict将一条或多条数据从缓存中删除

这些注解如何使用呢,我们使用一个简单的增删改查来使用一下:
Spring CacheContoller创建代理对象,有代理对象来实现这些注解中的方法,通过代理对象来操作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 {@Autowiredprivate 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缓存套餐

具体的实现思路如下:

  1. 导入Spring CacheRedis相关maven坐标
  2. 在启动类上加入@EnableCaching注解,开启缓存注解功能
  3. 在用户端接口SetmealControlerlist 方法上加入@Cacheable注解
  4. 在管理端接口SetmealControllersavedeleteupdatestartOrStop等方法上加入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使用步骤

  1. 导入maven坐标 spring-context(已存在,该包很小,包含在spring-context中)
  2. 启动类添加注解 @EnableScheduling开启任务调度
  3. 自定义定时任务类,要使用@Scheduled注解
@Component//需要实例化,交给Spring容器去管理
@Slf4j
public class MyTask {/*** 定时任务 每隔5秒触发一次*/@Scheduled(cron = "0/5 * * * * ?")public void executeTask(){//定时任务的处理逻辑方法log.info("定时任务开始执行:{}",new Date());}
}

订单状态定时处理

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

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

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

  1. 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消
  2. 通过定时任务每天凌晨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入门案例

实现步骤

  1. 直接使用websocket.html页面作为WebSocket客户端
  2. 导入WebSocket的maven坐标
  3. 导入WebSocket服务端组件WebSocketServer,用于和客户端通信
  4. 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
  5. 导入定时任务类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);//判断当前浏览器是否支持WebSocketif('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 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, @PathParam("sid") String sid) {System.out.println("收到来自客户端:" + sid + "的信息:" + message);}/*** 连接关闭调用的方法** @param sid*/@OnClosepublic 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 {@Beanpublic 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 {@Autowiredprivate 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注解)。

来单提醒与催单

  1. 通过WebSocket实现管理端页面和服务端保持长连接状态
  2. 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
  3. 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
  4. 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderld,content

当我们打开前端也面后,js代码执行WS请求,这里需要注意的时发起请求的端口是80,这是由于nginx反向代理转发实现的。

在这里插入图片描述

随后服务器端也接收到了请求并创建了连接

在这里插入图片描述
随后,在订单支付成功的业务中调用webSocketServer.sendToAllClient(JSON.toJSONString(map));来向客户端发送消息

    public void paySuccess(String outTradeNo) {
//        当前登录用户idLong 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());

整个下单服务逻辑代码如下:

	@Overridepublic 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 : '' };

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/1674.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Robbins-Monro(RM)算法【随机近似】

强化学习笔记 主要基于b站西湖大学赵世钰老师的【强化学习的数学原理】课程&#xff0c;个人觉得赵老师的课件深入浅出&#xff0c;很适合入门. 第一章 强化学习基本概念 第二章 贝尔曼方程 第三章 贝尔曼最优方程 第四章 值迭代和策略迭代 第五章 强化学习实践—GridWorld 第…

WP-AutoPostPro 汉化版: WordPress自动采集发布插件

WP-AutoPostPro 是目前最好用的WordPress自动采集发布插件&#xff0c;最大的特点是可以采集来自于任何网站的内容并自动发布到你的WordPress站点。真正做到可以采集任何网站的内容并自动发布&#xff0c;采集过程完全自动进行无需人工干预&#xff0c;并提供内容过滤、HTML标签…

libssh C++封装(一)

1 概述 libssh是一个在客户端和服务器端实现SSHv2协议的多平台C库。使用libssh&#xff0c;您可以远程执行程序、传输文件、使用安全透明的隧道、管理公钥等等。本文描述的对libssh客户端功能的C封装。 libssh下载地址 2 设计 2.1 类图 类型说明&#xff1a; Session SSH连接…

Centos7 的 Open Stack T 版搭建流程 --- (三)配置消息队列

配置消息队列 文章目录 配置消息队列&#xff08;1&#xff09;安装 RabbitMQ 服务并配置新用户权限controller &#xff08;2&#xff09;如何开启图形化&#xff08;拓展&#xff09; &#xff08;1&#xff09;安装 RabbitMQ 服务并配置新用户权限 controller yum install…

开源AI智能名片源码:虚实融合引领品牌营销新篇章

随着数字时代的飞速发展&#xff0c;品牌营销已经步入了一个全新的纪元。在这个变革的时代&#xff0c;开源AI智能名片源码以其独特的虚实融合功能&#xff0c;正引领着品牌营销走向更加智能化、个性化的道路。 传统的品牌营销往往局限于单向的信息传播&#xff0c;难以与用户产…

成都污水处理站运维厂家服务商

选择污水处理运维服务厂家时&#xff0c;需要考虑以下几个关键的事项来确保您选择了合适的服务提供商&#xff1a; 1. **资质和认证&#xff1a;** 确认厂家是否具备国家或地方政府颁发的相关环保和水处理行业资质、证书&#xff0c;比如ISO认证、水污染治理资质等&#xff0c;…

Nacos服务注册中心的下载与使用

1. Nacos是什么&#xff1f; https://nacos.io/ 官方&#xff1a;一个更易于构建云原生应用的动态服务发现(Nacos Discovery )、服务配置(Nacos Config)和服务管理平台。 集 注册中心配置中心服务管理 平台 Nacos 的关键特性包括: 服务发现和服务健康监测 动态配置服务 动…

手写一个Spring IOC框架

目录 一&#xff0c;Spring IOC 二&#xff0c;流程图设计 三&#xff0c;设计思路解析 三&#xff0c;开始写代码 1.准备工作: 2.扫描并加载类信息 3.初始化bean 4.测试一下 一&#xff0c;Spring IOC Spring IoC容器是Spring框架的核心&#xff0c;它通过读取配置信息…

【每日刷题】Day20

【每日刷题】Day20 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 面试题 17.04. 消失的数字 - 力扣&#xff08;LeetCode&#xff09; 2. 189. 轮转数组 - 力扣&#…

随机森林(Random Forests)

通过5个条件判定一件事情是否会发生&#xff0c;5个条件对这件事情是否发生的影响力不同&#xff0c;计算每个条件对这件事情发生的影响力多大&#xff0c;写一个随机森林&#xff08;Random Forests&#xff09;模型程序,最后打印5个条件分别的影响力。 ChatGPT 下面是一个使…

后端获取请求体Body,将请求体进行解密放回Request请求,并能通过@RequestBody获取

目前系统发送的post和put请求都是没有加密数据。客户需要将请求体加密。而系统已经基本开发完成&#xff0c;不可能一个一个去修改发送的请求。就需要在发送请求时候在拦截器中将body进行加密。并且在后端进行请求过滤解密&#xff0c;并且能通过RequestBody继续获取对象。 1.…

matlab学习004-使用matlab绘制两个序列及信号的卷积波形图

目录 1&#xff0c;序列&#xff1a;x(n)u(n-2)-u(n-10)&#xff0c;h(n)((0.9)^n)*u(n) 1&#xff09;前期基础 ①conv函数 ②离散单位阶跃信号 2&#xff09;波形图 3&#xff09;代码 ①使用input方法 ②代码改进【推荐使用】 2&#xff0c;信号&#xff1a;xu(…

C语言趣味代码(二)

1.珠玑妙算 1.1 介绍 《珠玑妙算》(Mastermind)是英国Invicta公司于1973年开始销售的一款益智游戏&#xff0c;据说迄今为止已经在全世界销售了5000万套。《珠玑妙算》于1974年获奖后&#xff0c;在1975年传入美国&#xff0c;1976年leslieH.Autl博士甚至还出版了一本名为The…

去雾笔记01-SRKTDN: Applying Super Resolution Method to Dehazing Task

文章目录 Abstract1. Introduction2. Related Work3. Method3.1. Network Architecture Abstract 们提出了一种结合超分辨方法和知识转移方法的模型。我们的模型由一个教师网络、一个去雾网络和一个超分辨率网络组成。 1. Introduction ECNU KT团队提出了一个知识蒸馏[20]模…

智慧养老平台|基于SprinBoot+vue的智慧养老平台系统(源码+数据库+文档)

智慧养老平台目录 基于SprinBootvue的外贸平台系统 一、前言 二、系统设计 三、系统功能设计 前台 后台 管理员功能 老人功能 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农…

控制台程序设置

该篇博客主要是对下一篇博客《贪吃蛇游戏》的设计做铺垫&#xff0c;大家可以持续关注(点个关注哦&#x1f60a;)等待更新&#xff0c;以下是个人主页&#xff1a; 敲上瘾-CSDN博客 在我们写的程序运行起来后弹出的框就是控制台程序。而这个窗口我们是可以对它进行设置的&…

终于找到恢复照片的绝佳方法了!手机照片恢复的看这里!

当今手机已经彻底离不开我们的生活&#xff0c;手机里面的数据也成为了我们高质量生活的重要构成&#xff0c;当手机内存爆满&#xff0c;我们就不得不采用清除部分手机数据的方法来释放空间。有一些数据删除后往往才发现还另有用处&#xff0c;比如手机里存储着的大量照片。 …

帆软报表实现通过js查询数据库设置表格数据

最近做的一直在做报表相关的需求&#xff0c;自己也是一边学一边做。有一个有意思的需求是在表格中某个单元格在编辑完以后其它的表格中的数据自动填充&#xff0c;当也是根据一定的规则与数据来源才能填充的。 先来点基础概念&#xff0c;就是帆软给我们提供了这个编辑后的事件…

学习笔记:Vue2高级篇

Vue2 学习笔记&#xff1a;Vue2基础篇_ljtxy.love的博客-CSDN博客学习笔记&#xff1a;Vue2中级篇_ljtxy.love的博客-CSDN博客学习笔记&#xff1a;Vue2高级篇_ljtxy.love的博客-CSDN博客 Vue3 学习笔记&#xff1a;Vue3_ljtxy.love的博客&#xff09;-CSDN博客 文章目录 7.…

《Spring》系列文章目录

Spring Framework是一个为基于Java的现代企业应用程序提供全面编程和配置模型的开源框架。它集成了控制反转&#xff08;IOC&#xff09;、依赖注入&#xff08;DI&#xff09;和面向切面编程&#xff08;AOP&#xff09;等容器技术。Spring框架的设计理念是面向Bean编程&#…