欢迎光临
我们一直在努力

♪苍穹外卖♪Day5 | 项目日记

今日完成内容

今天开始做 用户端 的功能,主要完成了 微信小程序登录、用户端 JWT 拦截器、以及 用户端商品浏览接口(分类、菜品、套餐),同时修复了 Redis 店铺状态的 key 名称问题。


一、微信小程序登录

1. 整体流程

小程序 wx.login() → 获取 code → 发送到后端 → 后端用 code 换 openid → 查库/注册 → 返回 JWT

2. 用户端 JWT 配置

管理端和用户端使用 不同的 JWT 密钥和 token 名称,互不影响:

# application.yml
sky:
jwt:
admin-secret-key: itcast
admin-ttl: 7200000
admin-token-name: token

user-secret-key: itheima
user-ttl: 7200000
user-token-name: authorization

JwtProperties 同时包含管理端和用户端的配置:

@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {
// 管理端
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
// 用户端
private String userSecretKey;
private long userTtl;
private String userTokenName;
}

3. 微信配置

# application-dev.yml
sky:
wechat:

4. UserController

用户端登录接口,接收小程序传来的 code,返回 JWT token:

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

@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;

@RequestMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("微信登录:{}", userLoginDTO.getCode());
// 微信登录
User user = userService.wxLogin(userLoginDTO);

//为微信用户生成令牌
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);
}
}

5. UserServiceImpl(核心逻辑)

@Override
public User wxLogin(UserLoginDTO userLoginDTO) {
// 1. 用 code 换 openid
String openid = getOpenid(userLoginDTO.getCode());

// 2. openid 为空说明登录失败
if (openid == null) {
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}

// 3. 判断是否为新用户
User user = userMapper.getByOpenid(openid);

// 4. 新用户自动注册
if (user == null) {
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}

// 5. 返回用户信息
return user;
}

6. 调用微信接口获取 openid

用 HttpClientUtil 发 GET 请求到微信的 jscode2session 接口:

private static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

private String getOpenid(String code) {
Map<String, String> map = new HashedMap<>();
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;
}

7. 数据流转过程

小程序端 code

UserController.login()

UserServiceImpl.wxLogin()
↓ getOpenid(code)
↓ 调用微信 jscode2session 接口
↓ 返回 openid
↓ 查库: userMapper.getByOpenid(openid)
↓ 新用户? → userMapper.insert(user)

返回 User 对象

UserController 生成 JWT token

返回 UserLoginVO (id, openid, token)


二、用户端 JWT 拦截器

新建 JwtTokenUserInterceptor,逻辑和管理端的 JwtTokenAdminInterceptor 基本一样,区别在于:

  • 用 userSecretKey 解析 token
  • 从请求头 authorization 获取 token
  • 提取的是 userId 而不是 empId

@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

@Autowired
private JwtProperties jwtProperties;

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 从请求头获取 token
String token = request.getHeader(jwtProperties.getUserTokenName());
try {
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
BaseContext.setCurrentId(userId);
return true;
} catch (Exception ex) {
response.setStatus(401);
return false;
}
}
}

在 WebMvcConfiguration 中注册拦截器:

protected void addInterceptors(InterceptorRegistry registry) {
// 管理端拦截器
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");

// 用户端拦截器
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login") // 登录接口放行
.excludePathPatterns("/user/shop/status"); // 店铺状态放行
}

用户端拦截器排除了两个路径:登录接口 和 店铺营业状态查询,这两个不需要 token。


三、用户端商品浏览接口

1. 分类查询(user/CategoryController)

直接复用了管理端 CategoryService.list() 方法,只返回启用状态的分类:

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

@Autowired
private CategoryService categoryService;

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

2. 菜品浏览(user/DishController)

根据分类 ID 查询菜品,只返回起售中的菜品,并且带上口味数据:

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

@Autowired
private DishService dishService;

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

这里调用的是新增的 listWithFlavor 方法,和管理端的 list 方法不同 —— 用户端需要看到菜品的口味选项。

3. 套餐浏览(user/SetmealController)

两个接口:

  • 根据分类查询套餐列表
  • 根据套餐 ID 查询包含的菜品列表

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

@Autowired
private SetmealService setmealService;

// 根据分类查询套餐(只返回起售中的)
@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查询包含的菜品
@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 新增了 listWithFlavor 方法:

/**
* 条件查询菜品和口味
*/

List<DishVO> listWithFlavor(Dish dish);

SetmealService 新增了两个方法:

/**
* 条件查询套餐
*/

List<Setmeal> list(Setmeal setmeal);

/**
* 根据套餐id查询菜品选项
*/

List<DishItemVO> getDishItemById(Long id);


四、Redis 店铺状态 key 修复

昨天 ShopController 里有个 bug —— set 和 get 操作用了字符串字面量 "KEY" 而不是常量 KEY(值为 "SHOP_STATUS")。今天统一修复为使用常量:

// 修复前
redisTemplate.opsForValue().set("KEY", status); // 存的 key 是字符串 "KEY"
redisTemplate.opsForValue().get("KEY"); // 取的 key 也是 "KEY"

// 修复后
redisTemplate.opsForValue().set(KEY, status); // 存的 key 是 "SHOP_STATUS"
redisTemplate.opsForValue().get(KEY); // 取的 key 也是 "SHOP_STATUS"


完成的功能列表

功能状态
微信小程序登录(code 换 openid + 自动注册) ✅ 已完成
用户端 JWT 配置(独立密钥和 token 名称) ✅ 已完成
用户端 JWT 拦截器 ✅ 已完成
用户端分类查询接口 ✅ 已完成
用户端菜品浏览(含口味,只返回起售中) ✅ 已完成
用户端套餐浏览(含菜品列表) ✅ 已完成
Redis 店铺状态 key 修复 ✅ 已完成

遇到的问题

1. 管理端和用户端同名类冲突

用户端和管理端都有 CategoryController、DishController、SetmealController、ShopController。解决办法:

  • 不同包:controller.admin 和 controller.user
  • @RestController("userXxxController") 指定不同的 Bean 名称

2. 用户端拦截器的放行路径

一开始忘记放行 /user/shop/status,导致用户端无法查看店铺营业状态。这个接口不需要登录就能访问,需要在拦截器中排除。

3. 微信接口返回的 openid 为空

如果微信的 appid 或 secret 配置错误,jscode2session 接口返回的 JSON 里没有 openid 字段,需要做判空处理并抛出登录失败异常。

4. 管理端和用户端 JWT 的区别

管理端用 adminSecretKey 签发,token 名称是 token;用户端用 userSecretKey 签发,token 名称是 authorization。两套密钥互不通用,管理端的 token 不能用来访问用户端接口,反之亦然。


学习收获

  • 微信小程序登录流程 —— 小程序端获取 code,后端用 appid + secret + code 调用微信接口换取 openid,再用 openid 查库或注册
  • 双端 JWT 隔离 —— 管理端和用户端使用不同的密钥和 token 名称,通过两个独立的拦截器分别校验,实现了安全隔离
  • Bean 名称冲突解决 —— 同名类放在不同包下,用 @RestController("beanName") 指定 Bean 名称
  • 新用户自动注册 —— 微信登录时如果 openid 在库中不存在,自动创建新用户,实现"登录即注册"
  • 复用已有 Service —— 用户端的分类查询直接复用了管理端的 CategoryService.list(),不需要重复写代码
  • 赞(0)
    未经允许不得转载:171主机测评 » ♪苍穹外卖♪Day5 | 项目日记
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址