今日完成内容
今天开始做 用户端 的功能,主要完成了 微信小程序登录、用户端 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 不能用来访问用户端接口,反之亦然。





