目录

学习目标

1. HttpClient

1.1 介绍

1.2 入门案例

1.2.1 GET方式请求

1.2.2 POST方式请求

2. 微信小程序开发

2.1 介绍

2.2 准备工作

2.3 入门案例

2.3.1 小程序目录结构

2.3.2 编写和编译小程序

2.3.3 发布小程序

3. 微信登录

3.1 导入小程序代码

3.2 微信登录流程(重点!!!)

3.3 需求分析和设计

3.3.1 产品原型

3.3.2 接口设计

3.3.3 表设计

3.4 代码开发

3.4.1 定义相关配置

3.4.2 DTO设计

3.4.3 VO设计

3.4.4 Controller层

3.4.6 Service层实现类

3.4.7 Mapper层

3.4.8 编写拦截器

3.5 功能测试

4. 导入商品浏览功能代码

4.1 需求分析和设计

4.1.1 产品原型

4.1.2 接口设计

4.2 代码实现

4.2.1 Mapper层

4.2.2 Service层

4.2.3 Controller层

4.3 功能测试


  • HttpClient

  • 微信小程序开发

  • 微信登录

  • 导入商品浏览功能代码

学习目标

  • 能够使用HttpClient发送HTTP请求并解析响应结果
  • 了解微信小程序开发过程
  • 掌握微信登录的流程并实现功能代码
  • 了解商品浏览功能需求

功能实现:微信登录商品浏览

微信登录效果图:

商品浏览效果图:

1. HttpClient

1.1 介绍

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

HttpClient作用:

  • 发送HTTP请求

  • 接收响应数据

我们知道Java一般是接收请求的,如Controller层,那么为什么要在Java程序中发送Http请求?有哪些应用场景呢?

HttpClient应用场景:

当我们在使用扫描支付、查看地图、获取验证码、查看天气等功能时

其实,应用程序本身并未实现这些功能,都是在应用程序里访问提供这些功能的服务,访问这些服务需要发送HTTP请求,并且接收响应数据,可通过HttpClient来实现。

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发送请求步骤:

  1. 创建HttpClient对象
  2. 创建HttpGet或者HttpPost对象,设置url访问地址--构造出请求路径、请求参数
  3. 使用HttpClient对象的execute方法发送请求,获取响应对象
  4. 解析响应获取数据
  5. 释放资源

1.2 入门案例

对HttpClient编程工具包有了一定了解后,那么,我们使用HttpClient在Java程序当中来构造Http的请求,并且把请求发送出去,接下来,就通过入门案例分别发送GET请求POST请求,具体来学习一下它的使用方法。

1.2.1 GET方式请求

正常来说,首先,应该导入HttpClient相关的坐标,但在项目中,就算不导入,也可以使用相关的API。

因为在项目中已经引入了aliyun-sdk-oss坐标:

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
</dependency>

上述依赖的底层已经包含了HttpClient相关依赖。

故选择导入或者不导入均可。

进入到sky-server模块,编写测试代码,发送GET请求。

实现步骤:

  1. 创建HttpClient对象

  2. 创建HttpGet请求对象--构造出请求路径、请求参数

  3. 使用HttpClient对象的execute方法发送请求,获取响应结果

  4. 解析结果,响应数据

  5. 关闭资源

/**
 * 测试HttpClient
 */
//@SpringBootTest
public class TestHttpClient {
    //需求1:通过HttpClient发送get请求,请求苍穹外卖用户端的查看店铺营业状态接口。/user/shop/status
    @Test
    public void testGet() throws Exception {
        //1.创建HttpClient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        //2.创建HttpGet对象,设置url访问地址--构造出请求路径、请求参数
        HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");

        //3.使用HttpClient对象的execute方法发送请求,获取响应对象
        CloseableHttpResponse response = httpClient.execute(httpGet);

        //4.解析响应,获取数据
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("statusCode = " + statusCode);
        HttpEntity entity = response.getEntity();
        String result = EntityUtils.toString(entity);
        System.out.println("result = " + result);

        //释放资源
        response.close();
        httpClient.close();
    }

在访问http://localhost:8080/user/shop/status请求时,需要提前启动项目,并且要保证营业状态为营业中。

测试结果:

1.2.2 POST方式请求

在HttpClientTest中添加POST方式请求方法,相比GET请求来说,POST请求若携带参数需要封装请求体对象,并将该对象设置在请求对象中。

实现步骤:

  1. 创建HttpClient对象
  2. 创建HttpPost对象,设置url访问地址
  3. 构造请求体参数,设置请求体编码以及类型
  4. 调用HttpCilent对象的execute方法,发送请求
  5. 解析响应数据
  6. 释放资源
//需求2:通过HttpClient发送post请求,请求苍穹外卖管理端的员工登录接口。/admin/employee/login
    @Test
    public void testPost() throws Exception {
        //1.创建HttpClient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        //2.创建HttpPost对象,设置url访问地址
        HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

        //2.1构造请求体参数
        Map map = new HashMap();
        map.put("username","admin");
        map.put("password","123456");
        StringEntity httpEntity = new StringEntity(JSON.toJSONString(map));
        //2.2设置请求体的编码以及类型 否则报错 "status":415,"error":"Unsupported Media Type"
        httpEntity.setContentType("application/json");
        httpEntity.setContentEncoding("utf-8");
        httpPost.setEntity(httpEntity);

        //3.调用HttpClient对象的execute方法,发送请求
        CloseableHttpResponse response = httpClient.execute(httpPost);

        //4.解析相应数据
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("statusCode = " + statusCode);
        String result = EntityUtils.toString(response.getEntity());
        System.out.println("result = " + result);

        //5.释放资源
        response.close();
        httpClient.close();
    }

测试结果:

2. 微信小程序开发

2.1 介绍

小程序是一种新的开放能力,开发者可以快速地开发一个小程序。可以在微信内被便捷地获取和传播,同时具有出色的使用体验。

官方网址:https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zh_CN

在进行小程序开发时,需要先去注册一个小程序。

2.2 准备工作

开发微信小程序之前需要做如下准备工作:

  • 注册小程序

  • 完善小程序信息

  • 下载开发者工具

注意:开发阶段,小程序发出请求到后端的Tomcat服务器,若不勾选,请求发送失败

2.3 入门案例

实际上,小程序的开发本质上属于前端开发,主要使用JavaScript开发,咱们现在的定位主要还是在后端,所以,对于小程序开发简单了解即可。

2.3.1 小程序目录结构

小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成,必须放在项目的根目录,如下:

文件说明:

app.js:必须存在,主要存放小程序的逻辑代码

app.json:必须存在,小程序配置文件,主要存放小程序的公共配置

app.wxss: 非必须存在,主要存放小程序公共样式表,类似于前端的CSS样式

对小程序主体三个文件了解后,其实一个小程序又有多个页面。比如说,有商品浏览页面、购物车的页面、订单支付的页面、商品的详情页面等等。那这些页面会放在哪呢? 会存放在pages目录。

每个小程序页面主要由四个文件组成:

文件说明:

js文件:必须存在,存放页面业务逻辑代码,编写的js代码。

wxml文件:必须存在,存放页面结构,主要是做页面布局,页面效果展示的,类似于HTML页面。

json文件:非必须,存放页面相关的配置。

wxss文件:非必须,存放页面样式表,相当于CSS文件。

2.3.2 编写和编译小程序

1). 编写

进入到index.wxml,编写页面布局

<view class="container">
  <view>{{msg}}</view>
   <view>
    <button type="default" bindtap="getUserInfo">获取用户信息</button>
    <image style="width: 100px;height: 100px;" src="{{avatarUrl}}"></image>
    {{nickName}}
  </view>
   <view>
    <button type="primary" bindtap="wxlogin">微信登录</button>
    授权码:{{code}}
  </view>
   <view>
    <button type="warn" bindtap="sendRequest">发送请求</button>
    响应结果:{{result}}
  </view>
</view>

进入到index.js,编写业务逻辑代码

Page({
  data:{
    msg:'hello world',
    avatarUrl:'',
    nickName:'',
    code:'',
    result:''
  },
  getUserInfo:function(){
    wx.getUserProfile({
      desc: '获取用户信息',
      success:(res) => {
        console.log(res)
        this.setData({
          avatarUrl:res.userInfo.avatarUrl,
          nickName:res.userInfo.nickName
        })
      }
    })
  },
  wxlogin:function(){
    wx.login({
      success: (res) => {
        console.log("授权码:"+res.code)
        this.setData({
          code:res.code
        })
      }
    })
  },
  sendRequest:function(){
    wx.request({
      url: 'http://localhost:8080/user/shop/status',
      method:'GET',
      success:(res) => {
        console.log("响应结果:" + res.data.data)
        this.setData({
          result:res.data.data
        })
      }
    })
  }})

运行效果

点击获取用户信息

点击微信登录

点击发送请求

因为请求http://localhost:8080/user/shop/status,先要启动后台项目。

2.3.3 发布小程序

小程序的代码都已经开发完毕,要将小程序发布上线,让所有的用户都能使用到这个小程序。

点击上传按钮:

指定版本号:

上传成功:

把代码上传到微信服务器就表示小程序已经发布了吗? 其实并不是。当前小程序版本只是一个开发版本。

进到微信公众平台,打开版本管理页面。

需提交审核,变成审核版本,审核通过后,进行发布,变成线上版本。

一旦成为线上版本,这就说明小程序就已经发布上线了,微信用户就可以在微信里面去搜索和使用这个小程序了。

3. 微信登录

3.1 导入小程序代码

开发微信小程序,本质上是属于前端的开发,我们的重点其实还是后端代码开发。所以,小程序的代码已经提供好了,直接导入到微信开发者工具当中,直接来使用就可以了。

3). 查看项目结构

主体的文件:app.js app.json app.wxss 项目的页面比较多,主要存放在pages目录。

4). 修改配置

因为小程序要请求后端服务,需要修改为自己后端服务的ip地址和端口号(默认不需要修改)

common-->vendor.js-->搜索(ctrl+f)-->baseUri

3.2 微信登录流程(重点!!!)

微信登录:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

流程图:

步骤分析:

  1. 小程序端,调用wx.login()获取code,就是授权码,只能用一次
  2. 小程序端,调用wx.request()发送请求并携带code,请求后端服务器
  3. Java后端,通过HttpClient向微信接口服务发送请求,并携带配置文件中的appid+appsecret+code三个参数
  4. Java后端,接收微信接口服务返回的数据,session_key+openid等,openid是微信用户的唯一标识
  5. Java后端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后续请求身份校验。
  6. 小程序端,收到JWT相关信息,存储storage。
  7. 小程序端,通过wx.request()发起业务请求,并携带token。
  8. Java后端,收到请求后,通过拦截器校验JWT,解析当前登录用户的id。
  9. Java后端,身份校验后,继续相关的业务逻辑处理,最后返回业务数据。

为什么小程序要通过服务器访问微信接口服务,而不直接访问第三方接口?

        小程序登录成功与否第三方接口说了算,那为什么还要经过java后端服务器呢?原因是后续发其他请求时,比如下单,查询商品,添加购物车,要在后端服务器去进行相关的操作,请求后端时我们要知道此时有没有登录,如果判断为已登录,才能执行相关的业务代码。那如何知道它有没有登录呢?我们要写校验逻辑,如拦截器、校验令牌会话技术,那么校验令牌我们首先要生成令牌,此时就体现了后端服务器的作用,必须先在Java后端服务器生成token,如果小程序跳过服务器直接去访问第三方接口,那根本不知道有没有登录,登不登录结果都是一样的,无法进行校验。

接下来,我们使用Postman进行测试。

说明:

  1. 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。

  2. 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key

之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

实现步骤:

1). 获取授权码

点击确定按钮,获取授权码code,每个授权码只能使用一次,每次测试,需重新获取。

2). 明确请求接口

请求方式、请求路径、请求参数

3). 发送请求

获取session_key和openid,这里我们可以用apifox进行测试,发送登录请求后,会获取到唯一的openid,注意这里的js_code参数只能用一次。

若出现code been used错误提示,说明授权码已被使用过,请重新获取

3.3 需求分析和设计

3.3.1 产品原型

用户进入到小程序的时候,微信授权登录之后才能点餐。需要获取当前微信用户的相关信息,比如昵称、头像等,这样才能够进入到小程序进行下单操作。是基于微信登录来实现小程序的登录功能,没有采用传统账户密码登录的方式。若第一次使用小程序来点餐,就是一个新用户,需要把这个新的用户保存到数据库当中完成自动注册。

登录功能原型图:

业务规则:

  • 基于微信登录实现小程序的登录功能

  • 如果是新用户需要自动完成注册

3.3.2 接口设计

如果要完成微信登录的话,最终就要获取微信用户的openid。在小程序端获取授权码后,向后端服务器发送请求,并携带授权码,这样后端服务器在收到授权码后,就可以去请求微信接口服务。最终,后端向小程序返回openid和token等数据。

基于上述的登录流程,就可以设计出该接口的请求参数返回数据

说明:请求路径/user/user/login,第一个user代表用户端,第二个user代表用户模块。

3.3.3 表设计

当用户第一次使用小程序时,会完成自动注册,把用户信息存储到user表中。

字段名 数据类型 说明 备注
id bigint 主键 自增
openid varchar(45) 微信用户的唯一标识
name varchar(32) 用户姓名
phone varchar(11) 手机号
sex varchar(2) 性别
id_number varchar(18) 身份证号
avatar varchar(500) 微信用户头像路径
create_time datetime 注册时间

说明:手机号字段比较特殊,个人身份注册的小程序没有权限获取到微信用户的手机号。如果是以企业的资质 注册的小程序就能够拿到微信用户的手机号。

数据库中表的主键最好用非业务字段,用单纯的数字字符当作字符串

3.4 代码开发

3.4.1 定义相关配置

配置微信登录所需配置项:

application-dev.yml

sky:
  wechat:
    appid: wxffb3637a228223b8
    secret: 84311df9199ecacdf4f12d27b6b9522d

application.yml

sky:
  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}

配置为微信用户生成jwt令牌时使用的配置项:

application.yml

sky:
  jwt:
    # 管理端jwt配置
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token

    #用户端jwt配置
    # 设置jwt签名加密时使用的秘钥
    user-secret-key: itheima
    # 设置jwt过期时间
    user-ttl: 7200000
    # 设置前端传递过来的令牌名称
    user-token-name: authentication

3.4.2 DTO设计

根据传入参数设计DTO类:

在sky-pojo模块,UserLoginDTO.java已定义

package com.sky.dto;

import lombok.Data;

import java.io.Serializable;

/**
 * C端用户登录
 */
@Data
public class UserLoginDTO implements Serializable {

    private String code; //微信授权码--只能使用一次

}

UserLoginDTO中就算只有一个属性也必须进行封装,因为code参数是微信授权码,只能使用一次,是json格式的,更规范,更符合Java的面向对象规则。

3.4.3 VO设计

根据返回数据设计VO类:

在sky-pojo模块,UserLoginVO.java已定义

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO implements Serializable {

    private Long id; //用户id
    private String openid; //微信openid
    private String token; //用户登录token

}

3.4.4 Controller层

根据接口定义创建UserController的login方法:

@RestController
@RequestMapping("/user/user")
@Slf4j
@Api(tags = "用户相关接口")
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @PostMapping("/login")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
        log.info("微信登录:{}", userLoginDTO);

        // 1.调用service的登陆方法
        User user = userService.login(userLoginDTO);

        //2.如果登录成功,生成令牌jwt
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID, user.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getUserSecretKey(),
                jwtProperties.getUserTtl(),
                claims);

        //3.构造UserLoginVO对象,返回Result
        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())
                .openid(user.getOpenid())
                .token(token)
                .build();

        return Result.success(userLoginVO);
    }
}

3.4.6 Service层实现类

创建UserServiceImpl实现类:实现获取微信用户的openid和微信登录功能

@Service
@Slf4j
public class UserServiceImpl implements UserService {

    @Autowired
    private WeChatProperties weChatProperties;

    @Autowired
    private UserMapper userMapper;

    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @Override
    public User login(UserLoginDTO userLoginDTO) {
        //1.通过HttpClient,构造登录凭证校验请求
        // 构造请求参数
        Map<String,String> paramMap = new HashMap<>();
        paramMap.put("appid", weChatProperties.getAppid());
        paramMap.put("secret", weChatProperties.getSecret());
        paramMap.put("js_code", userLoginDTO.getCode());
        paramMap.put("grant_type", "authorization_code");
        //调用HttpClientUtil工具类,发送请求
        String res = HttpClientUtil.doGet("https://api.weixin.qq.com/sns/jscode2session", paramMap);
        log.info("res={}",res);

        //2.解析相应结果将字符串转化为JSONObject类型,获取openid
        JSONObject jsonObject = JSON.parseObject(res);
        String openid = (String) jsonObject.get("openid");
        //2.1 判断openid是否为空,如果为空直接抛出登录异常,不进行下面的操作
        if (openid == null) {
            throw new LoginFailedException(MessageConstant.USER_NOT_LOGIN);
        }

        //3.根据接续结果获取的openid查询user表,判断是否为新用户
        User user = userMapper.selectByOpenid(openid);

        //4.如果是新用户,初始化用户数据到user表中
        if(user == null) {
            user = new User();
            user.setOpenid(openid);
            user.setCreateTime(LocalDateTime.now());
            user.setName(openid.substring(0,5));
            userMapper.insert(user);
        }

        //5.否则,直接返回user对象数据
        return user;
    }
}

3.4.7 Mapper层

创建UserMapper接口:

public interface UserMapper {

    /**
     * 根据openid查询用户
     * @param openid
     * @return
     */
    @Select("select * from user where openid = #{openid}")
    User selectByOpenid(String openid);

    /**
     * 插入数据
     * @param user
     */
    @Options(useGeneratedKeys = true,keyProperty = "id")
    @Insert("insert into user (openid, name, create_time) values (#{openid}, #{name}, #{createTime})")
    void insert(User user);
}

这里我的问题是我只在claims中put了一次,可是为什么他的size=2?

在使用 JWT生成令牌时,exp 是 JWT 标准中规定的一个保留声明,代表令牌的过期时间(Expiration Time)。

通常,在你使用的 JwtUtil.createJWT 方法内部,会自动为 claims 添加 exp 相关的声明。也就是说,虽然你只手动放入了 USER_ID,但生成 JWT 的工具方法在处理时,会根据配置(比如你代码里的 jwtProperties.getUserTtl(),它指定了令牌的有效时长),计算出过期时间,并将 exp 键值对添加到 claims 中,所以最终 claims 的大小是 2。

这样一来,你手动放了 1 个键(USER_ID),方法自动加了 1 个键(exp),所以 claims.size() 最终是 2。

简单总结就是:exp 是 JWT 标准要求的 “过期时间声明”,而生成 JWT 的工具方法(JwtUtil.createJWT)会自动帮你把 “过期时间” 塞进 claims 里。

我们再来看下面这张图,明明JWT工具类中参数 ttlMillis 已经存入了过期时间,那为什么claims还要存入一次过期时间?

1. JWT 的结构与 exp 的作用

JWT 分为三部分:Header(头部)、Payload(载荷)、Signature(签名)。其中:

  • Payload 用来存声明(比如用户 ID、过期时间等)。
  • exp 是 JWT 标准的保留声明,表示 “令牌过期时间”—— 服务器验证 JWT 时,会检查 exp,如果当前时间超过 exp 对应的时间,就认为令牌过期,拒绝请求(保证安全性)。

2. ttlMillis 和 claims 中 exp 的关系

你提到 createJWT 有 ttlMillis(Time To Live,存活时长,单位毫秒)参数,它是用来计算过期时间的,但最终 “过期时间” 需要以 exp 声明的形式,存入 JWT 的 Payload 中(也就是代码里的 claims,因为 claims 最终会被放到 Payload 里)。

long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);

这段代码是用ttlMillis计算出具体的过期时间点(当前时间 + 存活时长)。但这只是"计算出了时间",还需要把这个时间以exp为键,存入claims,这样 Jwts.builder() 才能把 exp 放到 JWT 的 Payload 里。

3. Jwts.builder() 的逻辑

Jwts.builder() 是用来构建 JWT 的工具。它的工作逻辑是:

  • 先通过 .setClaims(claims),把你传入的 claims(包含自定义声明,以及这里要加的 exp)放到 Payload 里。

  • 再通过其他方法(比如设置签名算法、密钥等),构建完整的 JWT。

如果不把 exp 放到 claims 里,Jwts.builder() 就不知道 “过期时间” 是什么,生成的 JWT 也不会包含 exp 声明 —— 这样服务器验证时,就无法判断令牌是否过期,安全性会大打折扣。

简单总结:ttlMillis 是 “计算过期时间的原材料”,而 claims 里的 exp 是 “把计算好的过期时间,存到 JWT 里的载体”—— 只有把 exp 放到 claims 里,JWT 才能携带 “过期时间” 的信息,让服务器验证时能判断令牌是否有效。

3.4.8 编写拦截器

编写拦截器JwtTokenUserInterceptor:统一拦截用户端发送的请求并进行jwt校验

/**
 * 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 {
        log.info("JwtTokenUserInterceptor:线程id={}",Thread.currentThread().getId());
        //判断当前拦截到的是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());
            //将获取到的登录人员id存入到TreadLocal中,方便后续使用
            BaseContext.setCurrentId(userId);
            log.info("用户id:{}", userId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            log.error("令牌解析失败:{}", ex.getMessage());
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

在WebMvcConfiguration配置类中注册拦截器:

/**
     * 注册自定义拦截器
     *
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        //注册管理端拦截器
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");

        //注册用户端拦截器
        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
    }

用户端拦截器排除这两个路径的原因如下:

  • /user/user/login:这是用户登录接口,用户在未登录状态下需要访问此接口进行身份认证,获取访问令牌(token)。如果对此接口也进行拦截验证,会导致用户无法登录系统。
  • /user/shop/status:这是获取店铺状态的公开接口,通常用于用户在不登录的情况下查看店铺的营业状态等基础信息。此类信息公开给所有用户访问,不需要进行身份验证。

3.5 功能测试

重新编译小程序,进行登录,获取到openid和token数据

我们在测试拦截器是否生效时,可以在UserController写一个模拟请求,在apifox中进行测试

  //配合apifox测试拦截器是否生效
   @GetMapping("/test")
   public Result test(){
        log.info("test.....");
        return Result.success();  
 }

测试设置请求头token时是否能够被拦截器拦截,然后再测试给他正确的请求头token之后是否能够通过拦截器。

4. 导入商品浏览功能代码

4.1 需求分析和设计

4.1.1 产品原型

用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示按钮,否则显示按钮。

4.1.2 接口设计

根据上述原型图先粗粒度设计接口,共包含4个接口。

接口设计:

  • 查询分类

  • 根据分类id查询菜品

  • 根据分类id查询套餐

  • 根据套餐id查询包含的菜品

接下来细粒度分析每个接口,明确每个接口的请求方式、请求路径、传入参数和返回值。

1). 查询分类

2). 根据分类id查询菜品

3). 根据分类id查询套餐

4). 根据套餐id查询包含的菜品

4.2 代码实现

4.2.1 Mapper层

在SetmealMapper.java中添加list和getDishItemBySetmealId两个方法

/**
     * 动态条件查询套餐
     * @param setmeal
     * @return
     */
    List<Setmeal> list(Setmeal setmeal);

    /**
     * 根据套餐id查询菜品选项
     * @param setmealId
     * @return
     */
    @Select("select sd.name, sd.copies, d.image, d.description " +
            "from setmeal_dish sd left join dish d on sd.dish_id = d.id " +
            "where sd.setmeal_id = #{setmealId}")
    List<DishItemVO> getDishItemBySetmealId(Long setmealId);

创建SetmealMapper.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealMapper">

    <select id="list" parameterType="Setmeal" resultType="Setmeal">
        select * from setmeal
        <where>
            <if test="name != null">
                and name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
    </select>
</mapper>

4.2.2 Service层

创建SetmealService.java

public interface SetmealService {

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    List<Setmeal> list(Setmeal setmeal);

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    List<DishItemVO> getDishItemById(Long id);

}

创建SetmealServiceImpl.java

/**
     * 根据分类id查询套餐
     * @param setmeal
     * @return
     */
    public List<Setmeal> list(Setmeal setmeal) {
        List<Setmeal> list = setmealMapper.list(setmeal);
        return list;
    }

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    public List<DishItemVO> getDishItemById(Long id) {
        return setmealMapper.getDishItemBySetmealId(id);
    }

在DishServiceImpl.java中实现listWithFlavor方法

/**
     * 条件查询菜品和口味
     * @param dish
     * @return
     */
    public List<DishVO> listWithFlavor(Dish dish) {
        List<Dish> dishList = dishMapper.listBy(dish);

        List<DishVO> dishVOList = new ArrayList<>();

        for (Dish d : dishList) {
            DishVO dishVO = new DishVO();
            BeanUtils.copyProperties(d,dishVO);

            //根据菜品id查询对应的口味
            List<DishFlavor> flavors = dishFlavorMapper.selectByDishId(d.getId());

            dishVO.setFlavors(flavors);
            dishVOList.add(dishVO);
        }

        return dishVOList;
    }

4.2.3 Controller层

创建DishController.java

@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
    @Autowired
    private DishService dishService;

    /**
     * 根据分类id查询菜品
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

        List<DishVO> list = dishService.listWithFlavor(dish);

        return Result.success(list);
    }

}

创建CategoryController.java

@RestController("userCategoryController")
@RequestMapping("/user/category")
@Api(tags = "C端-分类接口")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * 查询分类
     * @param type
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("查询分类")
    public Result<List<Category>> list(Integer type) {
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
}

创建SetmealController.java

@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    /**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    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);
    }

    /**
     * 根据套餐id查询包含的菜品列表
     *
     * @param id
     * @return
     */
    @GetMapping("/dish/{id}")
    @ApiOperation("根据套餐id查询包含的菜品列表")
    public Result<List<DishItemVO>> dishList(@PathVariable("id") Long id) {
        List<DishItemVO> list = setmealService.getDishItemById(id);
        return Result.success(list);
    }
}

4.3 功能测试

重启服务器、重新编译小程序

微信登录进入首页

菜品和套餐分类查询:

具体分类下的菜品查询:

菜品口味查询:

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐