微信小程序隶属于前端,因此我们只需要了解掌握一些基本的功能与业务逻辑即可。
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)
,而我们的项目此时部署在本地,是一种局域网