微信小程序开发

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

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

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

在这里插入图片描述

微信小程序基础语法

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

微信小程序开发

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

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

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

代码如下:

	@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 提供了一层抽象,底层可以切换不同的缓存实现,如:

  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 {

    @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缓存套餐

具体的实现思路如下:

  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);

    //判断当前浏览器是否支持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注解)。

来单提醒与催单

  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) {
//        当前登录用户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 : '' };

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

APP自动化测试-Android SDK SDK Manager.exe或者uiautomatorviewer.bat打不开,点击就一闪而已的原因

原因是找不到Java.exe的路径&#xff0c; 如果是uiautomatorviewer.bat打不开&#xff0c;则使用文本编辑器打开它&#xff0c;然后添加java安装路径 set java_exeC:\Program Files\Java\jdk1.8.0_321\bin\java.exe 同理&#xff1a; 如果是SDK Manager.exe和AVD Manager.ex…

一个不同长度元素排序找行和列的需求

1、需求&#xff1a;三种长度的元素&#xff0c;分别是4、8、12&#xff0c;每一行的长度是12&#xff0c;超过12就排到下一行&#xff0c;我们将这三种类型的多个元素打乱&#xff0c;然后找到这些元素对应的行和列。 如下图&#xff1a; 2、解决思路&#xff1a; 创建一个长…

Java中的构造器

即使在类中什么都不写也会自动的生成一个构造器 注意 使用new关键字是在调用构造器 如果定义了有参构造 那么就不会默认的走Person person new Person();如果没有自己手动的定义无参构造就不能使用 在idea中 用按键Altinsert可以快速生成有参、无参构造&#xff08;某些品牌的…

SpringBoot:SpringMVC(下)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、RequestBody补充二、PathVariable三.RequestPart:四.ResponseBody五.CookieValue&#xff0c;SessionAttribute&#xff0c;RequestHeader 前言 提示&…

window下qt可执行程序打包

添加软件图标 方法1&#xff1a; QApplication a(argc, argv);// 设置应用程序图标QIcon appIcon("C:/Users/Administrator/Desktop/otp.png"); // 替换为你的图标文件路径a.setWindowIcon(appIcon); 缺点&#xff1a;运行后才显示图标&#xff0c;可执行程序文件图…

集成触发器(数电笔记)

同步触发器&#xff1a; 主从触发器&#xff1a; 边沿触发器&#xff1a;

2024年怎么购买阿里云5年云服务器?购买5年有什么优惠?(图文教程)

2024年阿里云优惠活动中的服务器大多数都是针对新用户&#xff0c;而且时长都变成了1个月或者1年&#xff0c;比较符合大多数用户的需求&#xff0c;不过有些用户由于需要长期使用云服务器&#xff0c;需要一次买5年&#xff0c;但是现在活动中的云服务器又没有5年的了&#xf…

边缘计算的优势

边缘计算的优势 边缘计算是一种在数据生成地点附近处理数据的技术&#xff0c;而非传统的将数据发送到远端数据中心或云进行处理。这种计算模式对于需要快速响应的场景特别有效&#xff0c;以下详述了边缘计算的核心优势。 1. 降低延迟 边缘计算通过在数据源近处处理数据&…

opencv基础篇 ——(三)图像二值化

opencv基础篇 ——&#xff08;三&#xff09;图像二值化 图像二值化是图像处理中常用的一种技术&#xff0c;用于将灰度图像转换为只包含两个像素值&#xff08;通常是黑色和白色&#xff09;的二值图像。这种处理通常用于简化图像、减少数据量以及强调感兴趣的目标。 二值化…

vue3中web前端JS动画案例(二)多物体运动-多值运动

<script setup> import { ref, onMounted, watch } from vue // ----------------------- 01 js 动画介绍--------------------- // 1、匀速运动 // 2、缓动运动&#xff08;常见&#xff09; // 3、透明度运动 // 4、多物体运动 // 5、多值动画// 6、自己的动画框架 // …

OWASP发布十大开源软件安全风险及应对指南

​ 最近爆发的XZ后门事件&#xff0c;尽管未酿成Log4j那样的灾难性后果&#xff0c;但它再次敲响了警钟&#xff1a;软件供应链严重依赖开源软件&#xff0c;导致现代数字生态系统极其脆弱。面对层出不穷的安全漏洞&#xff0c;我们需要关注开源软件 (OSS)风险 &#xff0c;改进…

干货 | 用几个语法打破你对Python的滤镜

Python现在是越来越火爆&#xff0c;不仅是风靡世界&#xff0c;还直接进入了中小学生的课堂。所以有越来越多的人想要尝试编程了。 想到以前当我第一次用代码打出“Hello, world”的时候&#xff0c;那种兴奋激动之情&#xff0c;真的是难以言表。 不过很多同学在刚入门的时…

nodejs在控制台打印艺术字

const figlet require("figlet");figlet("SUCCESS", function (err, data) {if (err) {console.log("Something went wrong...");console.dir(err);return;}console.log(data);}); 参考链接&#xff1a; https://www.npmjs.com/package/figlet…

Facebook账号运营要用什么IP?

众所周知&#xff0c;Facebook封号大多数情况都是因为IP的原因。Facebook对于用户账号有严格的IP要求和限制&#xff0c;以维护平台的稳定性和安全性。在这种背景下&#xff0c;海外IP代理成为了一种有效的解决方案&#xff0c;帮助用户避免检测&#xff0c;更加快捷安全地进行…

【LeetCode: 39. 组合总和 + 递归】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

程序员缓解工作压力的小窍门

目录 程序员缓解工作压力的小窍门 方向一&#xff1a;工作与休息的平衡 方向二&#xff1a;心理健康与自我关怀 方向三&#xff1a;社交与网络建设 程序员缓解工作压力的小窍门 程序员的工作性质常常伴随着高度的精神集中和持续的创新压力。为了保持高效和创新&#xff0c…

算法题解记录20+++

题目描述&#xff1a; 难度&#xff1a;简单 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来…

伪逆矩阵的两种求法

当矩阵的行列不相等&#xff0c;矩阵不是方阵时&#xff0c;求解矩阵的逆&#xff0c;可以使用伪逆的方法。 求解伪逆有两种方式。本文以mxn&#xff08;m<n&#xff09;的矩阵求解为例。 方法1&#xff1a;右伪逆 对于一个矩阵 和矩阵&#xff0c;如何矩阵之间满足&#…

3D模型人物换装系统(五 模型核批之后模型uv不正确)模型UV不正确

3D模型人物换装系统&#xff08;五 模型核批之后模型uv不正确&#xff09;模型UV不正确 介绍展示Maya导入查看uvUnity中测试分析没合批为什么没有问题总结 介绍 最近在公司里给公司做模型优化合批的时候发现了模型的uv在合批之后无法正常展示&#xff0c;这里找了很多的原因&a…

hadoop安装记录

零、版本说明 centos [rootnode1 ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core)jdk [rootnode1 ~]# java -version java version "1.8.0_311" Java(TM) SE Runtime Environment (build 1.8.0_311-b11) Java HotSpot(TM) 64-Bit Server VM (…