四、day04作业
完成套餐管理端模块所有业务功能,包括:
- 新增套餐
- 套餐分页查询
- 删除套餐
- 修改套餐
- 起售停售套餐
- 根据id查询套餐
①、controller层
java
@RestController("adminSetmealController")
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
// 新增套餐
@PostMapping
public Result save(@RequestBody SetmealDTO setmealDTO){
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
// 套餐分页查询
@GetMapping("/page")
// 分页查询返回的结果是 total和 records
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO){
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}
// 删除套餐
@DeleteMapping
// 也可写成 @RequestParam List<Long> ids
public Result delete(List<Long> ids){
setmealService.deleteBatch(ids);
return Result.success();
}
// 修改套餐
@PutMapping
public Result update(@RequestBody SetmealDTO setmealDTO){
setmealService.update(setmealDTO);
return Result.success();
}
// 起售、停售套餐
@PostMapping("/status/{status}")
public Result updateStatus(@PathVariable("status") Integer status,Long id){
setmealService.startOrStop(status,id);
return Result.success();
}
// 根据套餐 id 查询套餐和其中关联的菜品
@GetMapping("/{id}")
public Result<SetmealVO> getById(@PathVariable("id") Long id){
SetmealVO setmealVO = setmealService.getByIdWithDish(id);
return Result.success(setmealVO);
}
}
②、service层
java
public interface SetmealService {
// 新增套餐
void saveWithDish(SetmealDTO setmealDTO);
// 套餐分页查询
PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
// 删除套餐
void deleteBatch(List<Long> ids);
// 修改套餐
void update(SetmealDTO setmealDTO);
// 起售、停售套餐
void startOrStop(Integer status,Long id);
// 根据套餐 id 查询套餐和其中关联的菜品
SetmealVO getByIdWithDish(Long setmealId);
// 将起售菜品中带有分类 id 的实体类套餐返回
List<Setmeal> list(Setmeal setmeal);
//根据套餐 id查询包含的菜品列表
List<DishItemVO> getDishItemById(Long id);
}
java
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {
@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private DishMapper dishMapper;
// 新增套餐,同时需要保存套餐和菜品的关联关系,对多张表操作需保证事务原子性
@Transactional
public void saveWithDish(SetmealDTO setmealDTO) {
// 1、向套餐表中插入新套餐
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
setmealMapper.insert(setmeal);
// 2、保存套餐和菜品的关联关系
// 获取插入的新套餐生成的套餐 id
Long setmealId = setmeal.getId();
// 获取套餐菜品关系
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
// 将新的套餐 id 赋入套餐菜品关系中
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
// 批量保存套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);
}
// 套餐分页查询
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO){
// 1、使用 PageHelper 工具类进行分页查询
// 第一个参数为当前页码,第二个参数为每页显示记录数
PageHelper.startPage(setmealPageQueryDTO.getPage(), setmealPageQueryDTO.getPageSize());
// 2、调用 mapper 持久层去进行数据库的查询
// 注意必须返回的是 Page<SetmealVO> 类型,而不是 List<SetmealVO> 类型,这是使用 PageHelper 工具类的要求
// 因为 PageHelper 工具类会自动将查询结果封装为 Page<SetmealVO> 类型
Page<SetmealVO> page = setmealMapper.myPageQuery(setmealPageQueryDTO);
// 3、将 page 分页查询结果包装为 PageResult 类型
// 第一个参数为总记录数,第二个参数为当前页记录列表
long total = page.getTotal();
List<SetmealVO> dishList = page.getResult();
// 4、返回 PageResult 类型的分页查询结果
return new PageResult(total, dishList);
}
// 删除套餐,同时需要删除两张表的数据,需确保事务的原子一致性
@Transactional
public void deleteBatch(List<Long> ids){
for(Long id : ids){
Setmeal setmeal = setmealMapper.getById(id);
if(setmeal.getStatus().equals(StatusConstant.ENABLE)){
//起售中的套餐不能删除
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}
}
// 如果不在起售则可删除,除了套餐表中的数据,还要同时删除套餐菜品关系表的数据
setmealMapper.deleteBatch(ids);
setmealDishMapper.deleteBatch(ids);
}
// 修改套餐,同时对两张表进行修改,需确保事务的原子一致性
@Transactional
public void update(SetmealDTO setmealDTO) {
// 1、更新实体类套餐表
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
setmealMapper.updateSetmeal(setmeal);
// 2、更新套餐菜品关系表(本质是将原来的关系删除后再重新插入新的套餐菜品关系)
// 执行完套餐表的更新可得套餐 id
Long setmealId = setmealDTO.getId();
// 获取套餐菜品关系
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
// 2.1、为新的套餐菜品表插入套餐表更新后对应的套餐 id
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
// 2.2、删除套餐和菜品的关联关系
List<Long> ids = new ArrayList<>();
ids.add(setmealId);
setmealDishMapper.deleteBatch(ids);
// 2.3、重新插入套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);
}
// 起售、停售套餐
// 本质是通过 id 找到对应套餐进行 status 的更新
public void startOrStop(Integer status,Long setmealId){
// 起售套餐前,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含不可起售菜品,无法起售"
if(status.equals(StatusConstant.ENABLE)){
// select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?
// 通过 id 查询到对应套餐中的菜品
List<Dish> dishList = dishMapper.getBySetmealId(setmealId);
if(dishList != null && !dishList.isEmpty()){
for(Dish dish : dishList){
if(StatusConstant.DISABLE.equals(dish.getStatus())){
throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
}
}
}
}
Setmeal setmeal = Setmeal.builder()
.id(setmealId)
.status(status)
.updateTime(LocalDateTime.now())
.updateUser(BaseContext.getCurrentId())
.build();
setmealMapper.updateSetmeal(setmeal);
}
// 根据套餐 id 查询套餐和其中关联的菜品
public SetmealVO getByIdWithDish(Long setmealId){
Setmeal setmeal = setmealMapper.getById(setmealId);
SetmealVO setmealVO = new SetmealVO();
List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(setmealId);
BeanUtils.copyProperties(setmeal, setmealVO);
setmealVO.setSetmealDishes(setmealDishes);
return setmealVO;
}
// 将起售菜品中带有分类 id 的实体类套餐返回
public List<Setmeal> list(Setmeal setmeal) {
return setmealMapper.list(setmeal);
}
//根据套餐 id查询包含的菜品列表
public List<DishItemVO> getDishItemById(Long id) {
return setmealMapper.getDishItemBySetmealId(id);
}
}
③、mapper层
java
@Mapper
public interface SetmealMapper {
// 根据分类id查询套餐数量
@Select("select count(id) from setmeal where category_id = #{categoryId}")
Integer countByCategoryId(Long id);
// 新增套餐
@AutoFill(OperationType.INSERT)
void insert(Setmeal setmeal);
// 套餐分页查询
Page<SetmealVO> myPageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
// 删除套餐
void deleteBatch(List<Long> ids);
// 更新套餐
@AutoFill(OperationType.UPDATE)
void updateSetmeal(Setmeal setmeal);
// 根据 id 查询套餐
@Select("select * from setmeal where id = #{id}")
Setmeal getById(Long id);
// 动态条件查询套餐
// 将起售菜品中带有分类 id 的实体类套餐返回
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);
}
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">
<!-- 新增套餐 -->
<!-- useGeneratedKeys="true"代表开启主键自增,keyProperty="id"代表将自增的主键值赋值给实体类的id属性 -->
<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
insert into setmeal (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
</insert>
<!-- 分页查询 -->
<!-- 输入的参数是 SetmealPageQueryDTO,返回的结果是 SetmealVO,用于前端展示 -->
<select id="myPageQuery" parameterType="com.sky.dto.SetmealPageQueryDTO" resultType="com.sky.vo.SetmealVO">
select s.*, c.name as categoryName
from setmeal s
left outer join category c on s.category_id = c.id
<where>
<if test="name != null and name != ''"> and s.name like concat('%',#{name},'%') </if>
<if test="categoryId != null"> and s.category_id = #{categoryId} </if>
<if test="status != null"> and s.status = #{status} </if>
</where>
order by s.create_time desc
</select>
<!-- 批量删除套餐 -->
<delete id="deleteBatch">
delete from setmeal where id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
<!-- 更新套餐实体类 -->
<update id="updateSetmeal">
update setmeal
<set>
<if test="name != null"> name=#{name}, </if>
<if test="categoryId != null"> category_id=#{categoryId}, </if>
<if test="price != null"> price=#{price}, </if>
<if test="image != null"> image=#{image}, </if>
<if test="description != null"> description=#{description}, </if>
<if test="status != null"> status=#{status}, </if>
update_time=#{updateTime}, update_user=#{updateUser}
</set>
where id=#{id}
</update>
<!-- 将起售菜品中带有分类 id 的实体类套餐返回 -->
<select id="list" resultType="com.sky.entity.Setmeal">
select * from setmeal
<where>
<if test="name != null">
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>
java
@Mapper
public interface SetmealDishMapper {
// 批量保存套餐和菜品的关联关系
void insertBatch(List<SetmealDish> setmealDishes);
// 根据 id 批量删除套餐菜品表的数据
void deleteBatch(List<Long> ids);
// 根据套餐 id 查询关联菜品
@Select("select * from setmeal_dish where setmeal_id = #{setmealId}")
List<SetmealDish> getBySetmealId(Long setmealId);
// 通过传入的菜品 id 查询是否和其他套餐关联
List<Long> getSetmealIdsByDishIds(List<Long> ids);
}
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.SetmealDishMapper">
<!-- 批量保存套餐和菜品的关联关系 -->
<insert id="insertBatch" parameterType="list">
insert into setmeal_dish
(setmeal_id,dish_id,name,price,copies)
values
<foreach collection="setmealDishes" item="sd" separator=",">
(#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
</foreach>
</insert>
<!-- 根据 id 批量删除套餐菜品表的数据 -->
<delete id="deleteBatch">
delete from setmeal_dish where id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
<!-- 通过传入的菜品 id 查询是否和其他套餐关联 -->
<!-- 通过遍历 List<Long> ids,将其中的每个元素称作 id,以逗号分隔,(开头,)结尾,作为 SQL 语句的参数 -->
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
select setmeal_id from setmeal_dish where dish_id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
</mapper>
④、测试



五、day05
1、redis
Redis 是内存数据库 ,内存有限,不能当成 MySQL 那种"海量永久存储"来用。适合热点数据 或临时数据。
常用命令详见Redis的md笔记,这里只进行spring-data-redis的使用讲解
1.1、导入依赖
yml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1.2、配置数据源
yaml
spring:
# 其他无关的暂时省略
data:
redis:
host: ${sky.redis.host}
port: ${sky.redis.port}
password: ${sky.redis.password}
database: ${sky.redis.database}
yaml
sky:
# 其他无关的暂时省略
redis:
host: localhost
port: 6379
password: 123456
# 默认 redis 初始创建了16个数据库,默认是使用第 0 号数据库,这里我们使用 1 号数据库
database: 1
1.3、编写配置类
①、补充序列化
Ⅰ、各数据类型默认序列化器
| 操作类型 | 默认序列化器 | 存储示例 |
|---|---|---|
| Key | JdkSerializationRedisSerializer | \xac\xed\x00\x05t\x00\x03foo |
| Value | JdkSerializationRedisSerializer | 二进制Java序列化格式 |
| Hash Key | JdkSerializationRedisSerializer | 同Key |
| Hash Value | JdkSerializationRedisSerializer | 同Value |
| 所有ZSet/TX/管道操作 | JdkSerializationRedisSerializer | 二进制格式 |
Ⅱ、常用redis序列化策略
| 序列化器 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JdkSerializationRedisSerializer | Java原生支持 | 兼容性差、体积大、可读性差 | 不推荐使用 |
| StringRedisSerializer | 简单字符串、高效 | 只能处理String类型 | Key序列化、简单Value |
| Jackson2JsonRedisSerializer | 可读性好、跨语言 | 需要类有无参构造、反射开销 | 复杂对象存储 |
| GenericJackson2JsonRedisSerializer | 存储类信息、支持多类型 | 占用稍多空间 | 需要类型转换的场景 |
| OxmSerializer | XML格式、可读 | 效率低、体积大 | 需要XML格式的场景 |
②、配置类
java
// 配置类,用于自定义 RedisTemplate 序列化方式
// 标记这是一个 Spring 配置类,会被 Spring 扫描并加载
@Configuration
public class RedisConfiguration {
// 注册一个名为 redisTemplate 的 Bean,类型是 RedisTemplate<String, Object>
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 创建一个 RedisTemplate 实例 template
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 把连接工厂注入进来,建立与 Redis 的连接
redisTemplate.setConnectionFactory(factory);
// 使用 String 序列化 Key
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 使用 JSON 序列化 Value
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// Hash结构也使用 String 序列化 HashKey,JSON 序列化 HashValue
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
1.4、测试使用
java
// 注意测试类要和主程序包结构一致,避免扫描问题
package com.sky;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
public class SpringDataRedisTest {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
void StringTest() {
// 写入一条字符串数据
redisTemplate.opsForValue().set("name", "zhangsan");
// 读取一条字符串数据(默认是Object类型)
Object name = redisTemplate.opsForValue().get("name");
System.out.println(name);
}
}

2、店铺运营状态设置
2.1、客户端设置营业状态以及查询
java
@RestController("adminShopController") // 为了避免与 userShopController 冲突,添加前缀
@RequestMapping("/shop")
@Slf4j
public class ShopController {
public static final String KEY = "SHOP_STATUS";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@PutMapping("/{status}")
// 设置营业状态
// 对返回值没有特别要求故 Result 不需要额外泛型
public Result setStatus(@PathVariable("status") Integer status) {
redisTemplate.opsForValue().set(KEY, status);
log.info("设置营业状态为:{}", status);
return Result.success();
}
@GetMapping("/status")
// 客户端获取营业状态
// 对返回值 data 有要求,故 Result 需额外泛型 Integer
public Result<Integer> getStatus() {
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
return Result.success(status);
}
}
2.2、用户端查询营业状态
java
@RestController("userShopController")
@RequestMapping("/user/shop")
public class ShopController {
public static final String KEY = "SHOP_STATUS";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/status")
// 用户端获取营业状态
// 对返回值 data 有要求,故 Result 需额外泛型 Integer
public Result<Integer> getStatus() {
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
return Result.success(status);
}
}
理论上用户端查询营业状态的接口可以和管理端查询营业状态的接口相同,不过由于请求网址不同还是做成两个
2.3、测试




六、day06
1、HttpClient
要想使用该功能需要导入对应的依赖,但由于之前文件上传功能导入了阿里云的oss的sdk,其中就包含了该依赖,故不需要额外引入响应的依赖坐标
在开发中是否需要单独编写一个 HTTP 客户端工具类,取决于具体场景和需求。虽然 Postman、IDEA 内置 HTTP Client 等工具在 测试阶段 非常方便,但在实际代码中编写 HTTP 工具类仍有其必要性。
1. 1、自动化流程需求
-
场景 :在代码中需要 动态发起 HTTP 请求(如调用第三方 API、微服务间通信)。
-
工具限制:Postman 是手动测试工具,无法集成到代码逻辑中。
1.2、定制化功能
- 需求:统一处理超时、重试、签名、加密、日志等逻辑。
1.3、性能优化
- 连接池管理:复用 HTTP 连接(如 Apache HttpClient 或 OkHttp 的连接池)。
- 工具限制:Postman 每次请求都是独立的,无法优化链路性能。
1.4、代码复用与维护
- 统一入口:所有 HTTP 调用通过同一工具类管理,便于升级(如更换 OkHttp → HttpClient)。
1.5、安全控制
- 敏感信息:Token、密钥等不应硬编码在 Postman 中,而应通过代码动态获取(如从 Vault 或数据库读取)
即postman等是在手动进行测试功能,当我们需要如在实现类中自动调用第三方api即可使用http工具类
2、微信小程序
2.1、注册微信小程序
- 搜"微信公众平台"
- 在底部小程序中找到入门指南
- 注册小程序
2.2、保存小程序id和密钥

2.3、下载开发者工具并创建项目
下载安装之后即可创建小程序项目

注意设置为不校验请求域名

2.4、目录结构
①、app.js
- 小程序入口文件,定义全局逻辑(如生命周期函数、全局变量)。
②、app.json
- 全局配置,包括页面路径、窗口样式、网络超时等。
③、app.wxss
类似css
- 全局样式,作用于所有页面(可选)。
④、页面文件(page.js/json/wxml/wxss)
- 每个页面由4个同名文件组成:
.js:页面逻辑。.json:页面单独配置(覆盖app.json的window设置)。.wxml:页面结构(支持数据绑定、条件渲染等)。.wxss:页面样式(仅对当前页面生效)。
⑤、project.config.json
- 项目个性化配置(如开发者工具设置、AppID等)。
⑥、sitemap.json
- 控制小程序页面是否允许被微信索引(SEO相关)。
由以上可见:微信小程序的开发类似前端的开发
2.5、简单测试
该简单测试主要是为了熟悉使用,以及设置获取授权码和发送请求的功能
js
// index.js
Page({
data: {
msg: '测试',
nickname: ''
},
onGetUserInfo(res) {
if (res.detail.userInfo) {
console.log("用户信息:", res.detail.userInfo);
this.setData({
nickname: res.detail.userInfo.nickName // 注意字段是nickName不是nickname
});
} else {
console.log("用户拒绝授权");
}
},
// 每个功能之间用,隔开
// 获取微信用户的临时授权码
wxLogin(){
wx.login({
success: (res)=>{
console.log(res.code)
}
})
},
// 发送请求
sendRequest(){
wx.request({
url: 'http://localhost:8080/user/shop/status',
method: 'GET',
success: (res)=>{
console.log(res.data)
}
})
}
});
html
<!--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>{{msg}}</view>
<view>昵称:{{nickname}}</view>
<!-- 根据微信最新政策,小程序无法直接获取用户微信资料(包括昵称、头像等敏感信息)返回的 userInfo 是匿名数据 -->
<button open-type="getUserInfo" bindgetuserinfo="OngetUserInfo" type="primary">获取用户信息</button>
<button bind:tap="wxLogin" type="warn">获取授权码</button>
<button bind:tap="sendRequest" type="default">发送请求</button>
</view>
</scroll-view>
注意:由于我之前修改了拦截路径为所有路径,但实际我们只需要拦截管理端,用户端不需要拦截,所以这里排除用户端路径
java
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
// 初始资料:
// registry.addInterceptor(jwtTokenAdminInterceptor)
// .addPathPatterns("/admin/**")
// .excludePathPatterns("/admin/employee/login");
// 注意由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
// 而不是初始的 /admin/employee/login
// 为了保证校验拦截器能够正常工作,需要修改拦截路径
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/employee/login")
// 由于我之前修改了拦截路径为所有路径,但实际我们只需要拦截管理端,用户端不需要拦截,所以这里排除用户端路径
.excludePathPatterns("/user/**");
}
}

3、微信登录
3.1、导入微信小程序代码

3.2、微信登陆
①、流程

-
微信小程序通过wx.login()方法获取临时授权码code(登录凭证)
-
微信小程序携带授权码发送请求给后端
-
后端携带参数调用微信提供的接口登录凭证校验服务
其中微信接口服务提供下面三个功能:
接口名称 请求路径 描述 小程序登录凭证校验 /sns/jscode2session 登录凭证校验 检验登录态 /wxa/checksession 校验服务器所保存的登录态 sessionkey 是否有效 重置登录态 /wxa/resetusersessionkey 重置指定的登录态 sessionkey GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JS_CODE&grant_type=GRANT_TYPE
| 参数 | 必填 | 说明 |
|---|---|---|
appid |
是 | 小程序唯一标识(从微信公众平台获取) |
secret |
是 | 小程序密钥(AppSecret,需妥善保管) |
js_code |
是 | 前端通过 wx.login() 获取的临时登录凭证(code) |
grant_type |
是 | 固定值 authorization_code |
- 微信接口服务返回参数
| 参数名 | 类型 | 说明 |
|---|---|---|
| session_key | string | 会话密钥 |
| unionid | string | 用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台帐号下会返回,详见 UnionID 机制说明。 |
| openid | string | 用户唯一标识 |
| errcode | number | 错误码,请求失败时返回 |
| errmsg | string | 错误信息,请求失败时返回 |
- 后端自定义登录的状态(要和微信接口服务返回的参数相关联)返回给微信小程序
- 微信小程序携带登录状态发出业务请求
- 查询返回的session_key、openid来返回数据
②、简单测试


③、配置文件
yaml
sky:
# 按理来说是要在配置文件中配置微信的 appid 和 secret,之后引用 dev 环境下的配置
# 但是这样不安全,完全可以使用系统环境变量注入的方式进行配置,使其不用在配置文件中明文配置
# wechat:
# appid: ${sky.wechat.appid}
# secret: ${sky.wechat.secret}
即application.yml总配置文件和application-dev.yml两个配置文件都不需要对微信的appid和secret进行明文配置,直接在系统环境进行配置,然后使用环境变量注入的方式进行注入即可
java
@Component
// 即可对应配置文件中 sky.wechat 开头的属性
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {
// 为避免在配置文件中明文配置微信的 appid 和 secret,这里和 oss 一样使用系统环境变量注入的方式进行配置
// 确保系统的环境变量中配置了 WECHAT_APPID 和 WECHAT_SECRET 即可
@Value("${WECHAT_APPID}")
private String appid; //小程序的appid
@Value("${WECHAT_SECRET}")
private String secret; //小程序的秘钥
private String mchid; //商户号
private String mchSerialNo; //商户API证书的证书序列号
private String privateKeyFilePath; //商户私钥文件
private String apiV3Key; //证书解密的密钥
private String weChatPayCertFilePath; //平台证书
private String notifyUrl; //支付成功的回调地址
private String refundNotifyUrl; //退款成功的回调地址
}
④、controller层
java
@RestController
@RequestMapping("/user/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;
@PostMapping("/login")
// 用户登录
// 请求参数是 json 格式的,包装为了 UserLoginDTO 对象
// 返回参数是 json 格式的,包装为了 UserLoginVO 对象,包含用户的登录凭证和其他信息
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("用户登录");
User user = userService.wxLogin(userLoginDTO);
// 登录成功后,调用自定义的工具类 JwtUtil 根据用户信息生成 JWT 令牌
String secretKey = jwtProperties.getUserSecretKey();
long ttl = jwtProperties.getUserTtl();
Map<String,Object> claims = new HashMap<>();
String token = JwtUtil.createJWT(
secretKey,
ttl,
claims
);
// 登录成功后,将 JWT 令牌封装到 UserLoginVO 对象中返回给前端
// 使用.builder() 比使用 set 方法更方便,且代码更易读
UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.openid(user.getOpenid())
.token(token)
.build();
return Result.success(userLoginVO);
}
}
⑤、service层
java
public interface UserService {
// 用户登录
User wxLogin(UserLoginDTO userLoginDTO);
}
java
@Service
@Slf4j
public class UserServiceImpl implements UserService {
// 微信提供的登陆验证的请求地址
private static final String WX_LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session";
private static final String GRANT_TYPE = "authorization_code";
@Autowired
private WeChatProperties weChatProperties;
@Autowired
private UserMapper userMapper;
// 微信登录
// 相较于传统登录,该方法是携带临时登录凭证 code 去调用微信服务的登录接口,获取用户的 openid 等信息
// 然后根据 openid 去数据库查询用户是否存在
// 如果不存在,则是新用户,需要完成自动注册并返回;如果存在,则直接返回用户信息
public User wxLogin(UserLoginDTO userLoginDTO) {
// // 1、调用微信服务的登录验证接口,获取用户的 openid 等信息
// // 自动调用第三方的 API 可使用 httpclient 或 okhttp 等工具类库
// // map 对应的是微信服务登录验证接口的请求参数
// Map<String,String> map = new HashMap<>();
// map.put("appid",weChatProperties.getAppid());
// map.put("secret",weChatProperties.getSecret());
// map.put("js_code",userLoginDTO.getCode());
// map.put("grant_type",GRANT_TYPE);
// // 返回的是一个 JSON 字符串,包含用户的 openid 等信息
// String response = HttpClientUtil.doGet(WX_LOGIN_URL,map);
// 1、调用微信服务的登录验证接口,获取用户的 openid 等信息(即将以上方法提取出来)
String response = getOpenid(userLoginDTO.getCode());
// 2、解析 JSON 字符串,获取用户的 openid 等信息
UserLoginVO userLoginVO = JSON.parseObject(response, UserLoginVO.class);
String openid = userLoginVO.getOpenid();
// 3、判断是否获取成功
if(openid == null){
log.info("调用微信服务登录验证接口失败,返回结果:{}",response);
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}
// 4、根据 openid 去数据库查询用户是否存在
User user = userMapper.getByOpenid(openid);
// 5、如果不存在,则是新用户,需要完成自动注册并返回;如果存在,则直接返回用户信息
if(user == null){
// 其他字段可后期在个人中心完善,这里仅填写必要字段
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}
return user;
}
private String getOpenid(String code) {
// 1、调用微信服务的登录验证接口,获取用户的 openid 等信息
// 自动调用第三方的 API 可使用 httpclient 或 okhttp 等工具类库
// map 对应的是微信服务登录验证接口的请求参数
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", GRANT_TYPE);
// 返回的是一个 JSON 字符串,包含用户的 openid 等信息
return HttpClientUtil.doGet(WX_LOGIN_URL, map);
}
}
⑥、mapper层
java
@Mapper
public interface UserMapper {
// 根据 openid 查询用户
@Select("select * from user where openid = #{openid}")
User getByOpenid(String openid);
// 新增用户
// 由于并不符合我们自定义的自动填充策略(不含 createUser 和 updateUser 字段),所以这里不使用 @AutoFill 注解
void insert(User user);
}
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.UserMapper">
<!-- 新增用户 -->
<!-- useGeneratedKeys="true"代表开启主键自增,keyProperty="id"代表将自增的主键值赋值给实体类的id属性 -->
<insert id="insert" parameterType="com.sky.entity.User" useGeneratedKeys="true" keyProperty="id">
insert into user (openid, name, phone, sex, id_number, avatar)
values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar})
</insert>
</mapper>
⑦、测试

即成功通过微信小程序获得到了临时的登陆凭证code,之后带着code发送请求给后端,后端带着code、、appid、appsecret去调用微信提供的登录校验api(第三方api),第三方api返回openid等信息给后端,后端带着openid去查数据库,进行业务判断
3.3、完善拦截器
在创建一个对应用户端的拦截器,并且注册即可
java
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
// 初始资料:
// registry.addInterceptor(jwtTokenAdminInterceptor)
// .addPathPatterns("/admin/**")
// .excludePathPatterns("/admin/employee/login");
// 注意由于 nginx 的反向代理配置,已将初始路径设置为:http://localhost/api/ 或者也可以设置为 http://localhost:8080/
// 而不是初始的 /admin/employee/login
// 为了保证校验拦截器能够正常工作,需要修改拦截路径
// 管理端拦截路径应为:/admin/**,但是由于
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/employee/login")
.excludePathPatterns("/user/**");
// 用户端拦截路径:/user/**
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}
}
4、导入商品浏览
导入相关代码后进行测试

注意如果导入后返回401,是 JWT 令牌校验出现问题,可能情况:
- 配置文件应设置:user-token-name: authentication
- JwtTokenUserInterceptor 拦截器确保改为了userid等
- 全局拦截器正确设置
- token在用户端登录时未正确生成(UserController 中token生成错误)