Java项目苍穹外卖:接口开发(2)

微信登录

HttpClient

介绍

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

HttpClient作用:发送HTTP请求,接收响应数据。

引入HttpClient依赖:(如果有引入aliyun-sdk-oss则不用额外引入了,aliyun-sdk-oss已经引入了HttpClient依赖)

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>

核心APIHttpClientHttpClientsCloseableHttpClientHttpGetHttpPost

发送请求步骤:

  1. 创建HttpClient对象。
  2. 创建Http请求对象。
  3. 调用HttpClientexecute方法发送请求。

案例

GET方式请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.sky.test;

public class HttpClientTest {
//测试通过HttpClient发送GET方式的请求
@Test
public void testGET() throws Exception {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();

//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");

//发送请求,接收响应结果
CloseableHttpResponse response = httpClient.execute(httpGet);//抛出异常

//获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("服务端返回的状态码为:" + statusCode);

HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("服务端返回的数据为:"+ body);

//关闭资源
response.close();
httpClient.close();
}
}
POST方式请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//测试通过HttpClient发送POST方式的请求
@Test
public void testPOST() throws Exception {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();

//创建请求对象
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

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 body = EntityUtils.toString(entity1);
System.out.println("服务端返回的数据为:"+ body);

//关闭资源
response.close();
httpClient.close();
}

实现工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.sky.test;

import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
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;

public class HttpClientTest {
//测试通过HttpClient发送GET方式的请求
@Test
public void testGET() throws Exception {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();

//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");

//发送请求,接收响应结果
CloseableHttpResponse response = httpClient.execute(httpGet);//抛出异常

//获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("服务端返回的状态码为:" + statusCode);

HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("服务端返回的数据为:"+ body);

//关闭资源
response.close();
httpClient.close();
}

//测试通过HttpClient发送POST方式的请求
@Test
public void testPOST() throws Exception {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();

//创建请求对象
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

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 body = EntityUtils.toString(entity1);
System.out.println("服务端返回的数据为:"+ body);

//关闭资源
response.close();
httpClient.close();
}
}

微信小程序开发

介绍

微信小程序:https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zh_CN

微信小程序注册地址:https://mp.weixin.qq.com/wxopen/waregister?action=step1

登录小程序后台:https://mp.weixin.qq.com/

查看小程序的AppID

微信小程序开发者工具下载地址: https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html

开发者工具创建小程序:

开发者工具设置不校验合法域名:需要勾选不校验合法域名,否则小程序无法向Tomcat发出请求。

入门案例

小程序目录结构

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

一个小程序页面由四个文件组成:

案例

注意:如果点击获取用户信息无法弹出弹窗,可以通过详情->本地设置->调试基础库,将当前版本修改为2.27.0以下版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--index.wxml-->
<navigation-bar title="Weixin" back="{{false}}" color="black" background="#FFF"></navigation-bar>
<scroll-view class="scrollarea" scroll-y type="list">
<view class="container">
<view>Weixin</view>
<view>
<button bindtap="getUserInfo" type="primary">获取用户信息</button>
</view>
<view>昵称:{{nickName}}</view>
<image src="{{url}}" style="width: 100px; height: 100px;"></image>

<view>
<button bindtap="wxLogin" type="warn">微信登录</button>
授权码:{{code}}
</view>

<view>
<button bindtap="sendRequest" type="default">发送请求</button>
</view>
</view>
</scroll-view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// index.js
Page({
data:{
msg: "hello surourou",
nickName: "",
url: "",
code: ""
},
//获取微信用户的头像和昵称
getUserInfo(){
console.log("调用方法")
wx.getUserProfile({
desc: '获取用户信息',
success: res => {
console.log(res.userInfo)
//为数据赋值
this.setData({
nickName: res.userInfo.nickName,
url: res.userInfo.avatarUrl
})
}
})
},

//微信登录,获取微信用户的授权码(授权码只能使用一次,每次调用都不一样)
wxLogin(){
wx.login({
success: (res) => {
console.log(res.code)
this.setData({
code: res.code
})
},
})
},

//发送请求
sendRequest(){
wx.request({
url: 'http://localhost:8080/user/shop/status',
method: 'GET',
success: (res) => {
console.log(res.data)
}
})
}
})

微信登录

导入小程序代码

微信登录流程

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

注意:可以使用postman测试登录凭证校验接口。

需求分析和设计

产品原型

业务规则:

  • 基于微信登录实现小程序的登录功能。
  • 如果是新用户需要自动完成注册。

接口设计

数据库设计(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 注册时间

代码开发

1.配置微信登录所需配置项。

application-dev.yml

1
2
3
4
sky:
wechat:
appid: wx356e117f7fb1a1ee
secret: 2b862ee13a45b6ea421c780702b66b96

application.yml

1
2
3
4
sky:
wechat:
appid: ${sky.wechat.appid}
secret: ${sky.wechat.secret}

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

application.yml

1
2
3
4
5
6
7
8
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
user-secret-key: itheima
# 设置jwt过期时间
user-ttl: 7200000
# 设置前端传递过来的令牌名称
user-token-name: authentication

3.DTO设计。

1
2
3
4
5
6
7
package com.sky.dto;

//C端用户登录
@Data
public class UserLoginDTO implements Serializable {
private String code;
}

4.VO设计。

1
2
3
4
5
6
7
8
9
10
11
package com.sky.vo;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO implements Serializable {
private Long id;
private String openid;
private String token;
}

5.根据接口定义创建UserControllerlogin方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.sky.controller.user;

@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;

//微信登录
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("微信用户登录:{}", userLoginDTO);
//微信登录
User user = userService.wxLogin(userLoginDTO);
//为微信用户生成jwt令牌
Map<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);
}
}

6.创建UserService接口。

1
2
3
4
5
6
7
package com.sky.service;

@Service
public interface UserService {
//微信登录
User wxLogin(UserLoginDTO userLoginDTO);
}

7.创建UserServiceImpl实现类。在UserServiceImpl中创建私有方法getOpenid。完善UserServiceImplwxLogin方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.sky.service.impl;

@Service
@Slf4j
public class UserServiceImpl implements UserService {
//微信服务接口地址
public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";
@Autowired
private WeChatProperties weChatProperties;
@Autowired
private UserMapper userMapper;

//微信登录
@Override
public User wxLogin(UserLoginDTO userLoginDTO) {
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;
}

//调用微信接口服务,获得微信用户的openid
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 = JSONObject.parseObject(json);
String openid = jsonObject.getString("openid");

return openid;
}
}

8.创建UserMapper接口。

1
2
3
4
5
6
7
8
9
10
11
package com.sky.mapper;

@Mapper
public interface UserMapper {
//根据openid查询用户
@Select("select * from user where openid = #{openid}")
User getByOpenid(String openid);

//插入数据
void insert(User user);
}

9.创建UserMapper.xml映射文件。

1
2
3
4
5
6
7
8
9
<?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.UserMapper">
<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>
</mapper>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.sky.interceptor;

//jwt令牌校验的拦截器
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

@Autowired
private JwtProperties jwtProperties;

//校验jwt
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是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());
log.info("当前用户id:", userId);
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}

11.在WebMvcConfiguration配置类中注册拦截器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;

//注册自定义拦截器
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");
}

导入商品浏览功能代码

产品原型

接口设计

查询分类

根据分类id查询菜品

根据分类id查询套餐

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

代码开发

1.CategoryController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.sky.controller.user;

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

2.DishController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.sky.controller.user;

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

}

3.SetmealController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.sky.controller.user;

@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.DishService

1
2
//条件查询菜品和口味
List<DishVO> listWithFlavor(Dish dish);

5.DishServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//条件查询菜品和口味
public List<DishVO> listWithFlavor(Dish dish) {
List<Dish> dishList = dishMapper.list(dish);

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

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

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

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

return dishVOList;
}

6.SetmealService

1
2
3
4
5
//条件查询
List<Setmeal> list(Setmeal setmeal);

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

7.SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
//条件查询
public List<Setmeal> list(Setmeal setmeal) {
List<Setmeal> list = setmealMapper.list(setmeal);
return list;
}

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

8.SetmealMapper

1
2
3
4
5
6
7
8
//动态条件查询套餐
List<Setmeal> list(Setmeal setmeal);

//根据套餐id查询菜品选项
@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);

9.SetmealMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>

缓存商品

缓存菜品

问题说明

用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。

结果:系统响应慢、用户体验差。

实现思路

通过Redis来缓存菜品数据,减少数据库查询操作。

缓存逻辑分析:

  • 每个分类下的菜品保存一份缓存数据。
  • 数据库中菜品数据有变更时清理缓存数据。

代码开发

添加缓存

修改用户端接口DishControllerlist方法,加入缓存处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.sky.controller.user;

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

/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@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);
//以什么方式存储就以什么方式get数据,得到Object类型,可以强转为存储时的数据类型
if(list != null && list.size() > 0) {
//如果存在,直接返回,无需查询数据库
return Result.success(list);
}


Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
//如果不存在,查询数据库,将查询到的数据放入redis中
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key, list);

return Result.success(list);
}
}
清理缓存

1.修改管理端接口DishController的相关方法,加入清理缓存的逻辑,需要改造的方法:

  • 新增菜品
  • 修改菜品
  • 批量删除菜品
  • 起售、停售菜品

2.抽取清理缓存的方法。

1
2
3
4
5
//清理缓存数据
private void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}

3.调用清理缓存的方法,保证数据一致性。

对于save(新增方法):

1
2
3
//清理缓存数据
String key = "dish_" + dishDTO.getCategoryId();
cleanCache(key);

对于deleteupdatestartOrStop方法:

1
2
//将所有的菜品缓存数据清理掉,所有以dish_开头的key
cleanCache("dish_*");

Spring Cache

介绍

Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache提供了一层抽象,底层可以切换不同的缓存实现,例如:

  • EHCache
  • Caffeine
  • Redis

引入Spring Cache依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.7.3</version>
</dependency>

常用注解

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

入门案例

导入资料中的初始工程,在此基础上加入Spring Cache注解即可。

在启动类加入@EnableCaching开启缓存注解功能。

1
@EnableCaching//开启缓存注解功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.itheima.controller;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

@Autowired
private UserMapper userMapper;

@PostMapping
@CachePut(cacheNames = "userCache", key = "#user.id") //如果使用Spring Cache缓存数据,key的生成:userCache::key
//key使用spEL(Spring EL),即Spring的表达式语言
//@CachePut(cacheNames = "userCache", key = "#result.id")//对象导航,result是返回值
//@CachePut(cacheNames = "userCache", key = "#p0.id")
//@CachePut(cacheNames = "userCache", key = "#a0.id")
//@CachePut(cacheNames = "userCache", key = "#root.args[0].id")//p0、a0和root.args[0]都是该方法的第一个参数
public User save(@RequestBody User user){
userMapper.insert(user);//插入时@Options(useGeneratedKeys = true,keyProperty = "id"),会返回id值到user中,可以通过user获得id值
return user;
}

@DeleteMapping
@CacheEvict(cacheNames = "userCache", key = "#id")
public void deleteById(Long id){
userMapper.deleteById(id);
}

@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache", allEntries = true)
public void deleteAll(){
userMapper.deleteAll();
}

@GetMapping
@Cacheable(cacheNames = "userCache", key = "#id")//key的生成:userCache::2
// Spring Cache的底层是基于代理技术,为当前Controller创建代理对象,在调用这个方法之前先进入代理对象,在代理对象中查询redis。
// 首先使用key去查询redis中是否存在缓存数据,如果存在缓存数据则直接使用,不会调用getById方法。
// 如果没有查询到则通过反射调用getById方法查询数据库,然后把数据返回,将返回结果存入redis缓存中。
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}

}

缓存套餐

实现思路

  1. 导入Spring Cache和Redis相关maven坐标。
  2. 在启动类上加入@EnableCaching注解,开启缓存注解功能。
  3. 在用户端接口SetmealControllerlist方法上加入@Cacheable注解。
  4. 在管理端接口SetmealControllersavedeleteupdatestartOrStop等方法上加入CacheEvict注解。

代码开发

1.在用户端接口SetmealControllerlist方法上加入@Cacheable注解。

1
@Cacheable(cacheNames = "setmealCache", key = "#categoryId")//key:categoryId::100

2.在管理端接口SetmealControllersavedeleteupdatestartOrStop等方法上加入CacheEvict注解。

1
2
3
4
//save
@CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId")//key:categoryId::100
//delete、update、startOrStop
@CacheEvict(cacheNames = "setmealCache", allEntries = true)

购物车

添加购物车

产品原型

接口设计

  • 请求方式:POST
  • 请求路径:/user/shoppingCart/add
  • 请求参数:套餐id、菜品id、口味
  • 返回结果:codedatamsg

数据库设计(shopping_cart表)

作用:暂时存放所选商品的地方,选的什么商品,每个商品都买了几个,不同用户的购物车需要区分开。

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 商品名称 冗余字段
image varchar(255) 商品图片路径 冗余字段
user_id bigint 用户id 逻辑外键
dish_id bigint 菜品id 逻辑外键
setmeal_id bigint 套餐id 逻辑外键
dish_flavor varchar(50) 菜品口味
number int 商品数量
amount decimal(10,2) 商品单价 冗余字段
create_time datetime 创建时间

代码开发

1.根据添加购物车接口的参数设计DTO

1
2
3
4
5
6
7
8
package com.sky.dto;

@Data
public class ShoppingCartDTO implements Serializable {
private Long dishId;
private Long setmealId;
private String dishFlavor;
}

2.根据添加购物车接口创建ShoppingCartController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.sky.controller.user;

@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "C端购物车相关接口")
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;

//添加购物车
@PostMapping("/add")
@ApiOperation("添加购物车")
public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("添加购物车,商品信息为:{}",shoppingCartDTO);
shoppingCartService.addShoppingCart(shoppingCartDTO);
return Result.success();
}
}

3.创建ShoppingCartService接口。

1
2
3
4
5
6
7
package com.sky.service;

@Service
public interface ShoppingCartService {
//添加购物车
void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}

4.创建ShoppingCartServiceImpl实现类,并实现add方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.sky.service.impl;

@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;

//添加购物车
@Override
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
//判断当前加入到购物车中的商品是否已经存在了
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);

List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);

//如果已经存在了,只需要将数量加一
if(list != null && list.size() > 0) {
ShoppingCart cart = list.get(0);//只会查找到一条
cart.setNumber(cart.getNumber() + 1);//update shopping_cart set number = ? where id = ?
shoppingCartMapper.updateNumberById(cart);
}else{//如果不存在,需要插入一条购物车数据
//判断本次添加到购物车的是菜品还是套餐
Long dishId = shoppingCartDTO.getDishId();
if(dishId != null) {
//本次添加到购物车的是菜品
Dish dish = dishMapper.getById(dishId);
shoppingCart.setName(dish.getName());
shoppingCart.setImage(dish.getImage());
shoppingCart.setAmount(dish.getPrice());
}else{
//本次添加到购物车的是套餐
Long setmealId = shoppingCartDTO.getSetmealId();
Setmeal setmeal = setmealMapper.getById(setmealId);
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}
}
}

5.创建ShoppingCartMapper接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.sky.mapper;

@Mapper
public interface ShoppingCartMapper {
//动态条件查询
List<ShoppingCart> list(ShoppingCart shoppingCart);

//根据id修改商品数量
@Update("update shopping_cart set number = #{number} where id = #{id};")
void updateNumberById(ShoppingCart shoppingCart);

//插入购物车数据
@Insert("insert into shopping_cart (name, user_id, dish_id, setmeal_id, dish_flavor, number, amount, image, create_time) " +
"values (#{name}, #{userId}, #{dishId}, #{setmealId}, #{dishFlavor}, #{number}, #{amount}, #{image}, #{createTime});")
void insert(ShoppingCart shoppingCart);
}

6.创建ShoppingCartMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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.ShoppingCartMapper">

<select id="list" resultType="com.sky.entity.ShoppingCart">
select * from shopping_cart
<where>
<if test="userId != null">and user_id = #{userId}</if>
<if test="setmealId != null">and setmeal_id = #{setmealId}</if>
<if test="dishId != null">and dish_id = #{dishId}</if>
<if test="dishFlavor != null">and dish_flavor = #{dishFlavor}</if>
</where>
</select>
</mapper>

查看购物车

产品原型

接口设计

代码开发

1.在ShoppingCartController中创建查看购物车的方法。

1
2
3
4
5
6
7
//查看购物车
@GetMapping("/list")
@ApiOperation("查看购物车")
public Result<List<ShoppingCart>> list(){
List<ShoppingCart> list = shoppingCartService.showShoppingCart();
return Result.success(list);
}

2.在ShoppingCartService接口中声明查看购物车的方法。

1
2
//查看购物车
List<ShoppingCart> showShoppingCart();

3.在ShoppingCartServiceImpl中实现查看购物车的方法。

1
2
3
4
5
6
7
8
9
//查看购物车
@Override
public List<ShoppingCart> showShoppingCart() {
//获取到当前微信用户的id
Long UserId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = ShoppingCart.builder().userId(UserId).build();
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
return list;
}

清空购物车

产品原型

接口设计

代码开发

1.在ShoppingCartController中创建清空购物车的方法。

1
2
3
4
5
6
7
//清空购物车
@DeleteMapping("/clean")
@ApiOperation("清空购物车")
public Result clean(){
shoppingCartService.cleanShoppingCart();
return Result.success();
}

2.在ShoppingCartService接口中声明清空购物车的方法。

1
2
//清空购物车
void cleanShoppingCart();

3.在ShoppingCartServiceImpl中实现清空购物车的方法。

1
2
3
4
5
6
7
//清空购物车
@Override
public void cleanShoppingCart() {
//获取到当前微信用户的id
Long UserId = BaseContext.getCurrentId();
shoppingCartMapper.deleteByUserId(UserId);
}

4.在ShoppingCartMapper接口中创建删除购物车数据的方法。

1
2
3
//根据用户id删除购物车数据
@Delete("delete from shopping_cart where user_id = #{userId}")
void deleteByUserId(Long userId);

删除购物车中一个商品

产品原型

接口设计

代码开发

1.ShoppingCartController

1
2
3
4
5
6
7
8
//删除购物车中一个商品
@PostMapping("/sub")
@ApiOperation("删除购物车中一个商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("删除购物车中一个商品,商品:{}", shoppingCartDTO);
shoppingCartService.subShoppingCart(shoppingCartDTO);
return Result.success();
}

2.ShoppingCartService

1
2
//删除购物车中一个商品
void subShoppingCart(ShoppingCartDTO shoppingCartDTO);

3.ShoppingCartServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//删除购物车中一个商品
public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
//设置查询条件,查询当前登录用户的购物车数据
shoppingCart.setUserId(BaseContext.getCurrentId());

List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);

if(list != null && list.size() > 0){
shoppingCart = list.get(0);

Integer number = shoppingCart.getNumber();
if(number == 1){
//当前商品在购物车中的份数为1,直接删除当前记录
shoppingCartMapper.deleteById(shoppingCart.getId());
}else {
//当前商品在购物车中的份数不为1,修改份数即可
shoppingCart.setNumber(shoppingCart.getNumber() - 1);
shoppingCartMapper.updateNumberById(shoppingCart);
}
}
}

4.ShoppingCartMapper

1
2
3
// 根据id删除购物车数据
@Delete("delete from shopping_cart where id = #{id}")
void deleteById(Long id);

Java项目苍穹外卖:接口开发(2)
http://surourou8.github.io/2024/11/21/Java项目苍穹外卖:接口开发(2)/
作者
Su Rourou
发布于
2024年11月21日
许可协议