1.网关统一登录校验
路由只是把请求“送过去”,并不能判断用户是否有资格访问。
真正的登录校验,要借助网关过滤器来完成。
前言
1.1.为什么用 GlobalFilter
网关中的过滤器有两种:
GatewayFilter:作用于某些特定路由GlobalFilter:作用于所有路由
登录鉴权属于整个系统统一入口逻辑,因此更适合用 GlobalFilter。
1.2.鉴权规则设计
当前项目采用一个简单明确的规则:
- 路径匹配
/api/**/auth/**的接口,必须登录 - 其它接口,允许匿名访问
为什么这样设计?
因为商城中本来就有两类接口:
- 不强制登录接口:首页、分类、商品列表、商品详情
- 必须登录接口:购物车、地址、收藏、下单、当前用户信息
只要路径规划提前统一,网关做判断就会很简单。
1.3.AuthGlobalFilter
package com.zx.gateway.filter;
import com.alibaba.fastjson2.JSON;
import com.zx.domain.vo.common.Result;
import com.zx.domain.vo.common.ResultCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@Slf4j
@Component
@Order(0)
public class AuthGlobalFilter implements GlobalFilter {
private static final String TOKEN_PREFIX = "user:login:token:";
@Autowired
private RedisTemplate<String, String> redisTemplate;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求路径
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
log.info("请求路径:{}", path);
// 2. 获取请求头 token
String token = request.getHeaders().getFirst("token");
log.info("请求头 token:{}", token);
// 3. 根据 token 查询 Redis 登录态
String userInfoJsonStr = "";
if (StringUtils.isNotBlank(token)) {
userInfoJsonStr = redisTemplate.opsForValue().get(TOKEN_PREFIX + token);
}
log.info("Redis用户信息:{}", userInfoJsonStr);
// 4. 如果登录态存在,则把用户信息放入请求头继续向下传递
if (StringUtils.isNotBlank(userInfoJsonStr)) {
ServerHttpRequest newRequest = request.mutate()
.headers(headers -> headers.add(
"userInfo",
URLEncoder.encode(userInfoJsonStr, StandardCharsets.UTF_8)
))
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
}
// 5. 如果当前接口是必须登录接口,则直接拦截
if (antPathMatcher.match("/api/**/auth/**", path)) {
log.info("命中受保护资源,且未登录");
return out(exchange.getResponse(), ResultCodeEnum.LOGIN_AUTH);
}
// 6. 非受保护接口,直接放行
return chain.filter(exchange);
}
private Mono<Void> out(ServerHttpResponse response, ResultCodeEnum resultCodeEnum) {
Result<?> result = Result.build(null, resultCodeEnum);
byte[] bits = JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
}
1.4.这段代码到底做了什么
按执行顺序理解:
- 先拿到请求路径
- 再看看请求头里有没有
token - 如果有,就去
Redis查当前登录态 - 查到了,说明用户已登录
- 已登录时,把用户信息塞进请求头
userInfo - 如果没登录,再判断当前是不是
/auth/保护接口 - 如果是保护接口,直接拦截
- 如果不是保护接口,就允许匿名访问
这里最关键的一步是第 5 步。
它解决了一个很核心的问题:
网关虽然已经知道“当前用户是谁”,但业务微服务还不知道。
因此必须把用户信息继续传给下游服务。
1.5.为什么要给 userInfo 做 URL 编码
原始 JSON 字符串里如果包含中文、空格或特殊符号,直接放进请求头很容易出现乱码或解析异常。
所以更稳妥的做法是:
- 网关写入请求头前先
URLEncoder.encode(...) - 下游微服务读取后再
URLDecoder.decode(...)
这个细节如果漏掉,调试时最容易出现“昵称中文乱码”的问题。
2.微服务中获取当前登录用户
网关已经把 userInfo 放进请求头了,但控制器方法仍然不会自动拿到这个对象。
因此还需要在每个业务微服务内部,把用户信息提取出来,存入一个“当前线程可直接访问的位置”。
这个位置通常就是 ThreadLocal。
前言
2.1.为什么要放进 ThreadLocal
因为后续很多业务都需要“当前登录用户”:
- 新增收货地址时,需要知道
userId - 收藏商品时,需要知道是谁在收藏
- 提交订单时,需要知道当前下单人
如果每个控制器方法都自己从请求头解析一次,会非常重复。
因此更常见的做法是:

- 进入服务时统一解析一次
- 存入
ThreadLocal - 任意业务代码都可以随时获取
2.2.AuthThreadLocalUtils
package com.zx.common;
import com.zx.domain.entity.user.UserInfo;
public class AuthThreadLocalUtils {
private static final ThreadLocal<UserInfo> THREAD_LOCAL = new ThreadLocal<>();
public static UserInfo get() {
return THREAD_LOCAL.get();
}
public static void set(UserInfo userInfo) {
THREAD_LOCAL.set(userInfo);
}
public static void clear() {
THREAD_LOCAL.remove();
}
}
2.3.公共过滤器 MyFilter
package com.zx.common;
import com.alibaba.fastjson.JSON;
import com.zx.domain.entity.user.UserInfo;
import com.zx.domain.vo.common.Result;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@Slf4j
@WebFilter(urlPatterns = "/*")
@Component
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String uri = request.getRequestURI();
String userInfoJson = request.getHeader("userInfo");
if (StringUtils.isNotBlank(userInfoJson)) {
userInfoJson = URLDecoder.decode(userInfoJson, StandardCharsets.UTF_8);
}
log.info("MyFilter uri:{} userInfoJson:{}", uri, userInfoJson);
try {
if (StringUtils.isBlank(userInfoJson) && uri.contains("/auth/")) {
response.setHeader("Content-Type", "application/json;charset=UTF-8");
Result<?> result = Result.build(null, 208, "请先登录");
response.getOutputStream()
.write(JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8));
return;
}
if (StringUtils.isNotBlank(userInfoJson)) {
UserInfo userInfo = JSON.parseObject(userInfoJson, UserInfo.class);
AuthThreadLocalUtils.set(userInfo);
}
filterChain.doFilter(servletRequest, servletResponse);
} finally {
// 非常重要:避免线程复用导致脏数据
AuthThreadLocalUtils.clear();
}
}
}
2.3.让过滤器生效
因为 MyFilter 放在 zx-common 中,所以业务微服务启动类需要加:
@ServletComponentScan("com.zx.common")
例如 ProductServiceApplication、UserServiceApplication 都要加上。
4.6.Controller 中如何使用
例如“获取当前登录用户信息”接口,可以直接从 ThreadLocal 中拿:
@Operation(summary = "获取当前登录用户信息")
@GetMapping("/auth/getCurrentUserInfo")
public Result<UserInfoVo> getCurrentUserInfo() {
UserInfo userInfo = AuthThreadLocalUtils.get();
UserInfoVo vo = new UserInfoVo();
vo.setNickName(userInfo.getNickName());
vo.setAvatar(userInfo.getAvatar());
return Result.build(vo, ResultCodeEnum.SUCCESS);
}
这样一来,控制层就不再需要手动:
- 从请求头取
token - 再查
Redis - 再自己解析 JSON
这就是“统一鉴权后,业务层更轻”的实际体现。
到这里,整个登录鉴权链路已经闭环,建议一定要自己顺着请求走一遍:👇👇
- 用户先登录
user-service生成token,并把登录信息写入Redis- 前端后续访问受保护接口时,把
token放入请求头 - 请求先进入
zx-gateway AuthGlobalFilter校验token- 校验通过后,把
userInfo写进请求头 - 请求到达
product-service或user-service MyFilter读取请求头中的userInfo- 将用户信息写入
ThreadLocal - 控制器或业务层直接从
AuthThreadLocalUtils获取当前用户
在上一节里,我们已经完成了登录鉴权、网关透传和 ThreadLocal 保存当前用户。
从这一节开始,这些“身份能力”终于要真正参与业务了。
最典型的两个场景就是:
- 收货地址必须知道“是谁在维护自己的地址”
- 商品收藏必须知道“是谁收藏了哪一个商品”
因此,本篇既是在继续开发商城业务,也是在训练你把“当前登录用户”真正用进业务代码中。
3.收货地址模块
登录成功后,用户最先会接触到的“个人数据”通常就是收货地址。
它看起来只是一个表单,但里面其实包含了很多典型业务规则:
- 数据只能操作当前登录用户自己的
- 必须维护默认地址
- 省市区联动需要单独接口支持
- 保存时不能只存编码,还要拼出完整地址,方便后续订单直接展示
前言
3.1.需求分析
先看页面效果,再决定接口怎么设计。


本节最终要完成 5 个能力:
- 查询当前用户地址列表
- 根据
id查询地址详情 - 根据
parentCode查询下级地区,实现三级联动 - 新增或修改收货地址
- 删除收货地址
为什么这里要先拆成这么多接口?
因为前端页面虽然只看到一个“地址管理”,但背后其实是多个动作:
- 打开页面,需要先拉地址列表
- 点击编辑,需要拉一条地址详情
- 切换省份,需要继续拉市列表
- 点提交,才是保存或修改
把动作拆开,后端职责会更清晰,排错也更容易。
3.2.查询当前用户地址列表
先做列表接口,因为它是地址页的入口。
3.2.1.接口说明
GET /api/user/userAddress/auth/findUserAddressList
请求头:
token: 当前登录用户令牌
这里路径里带有 auth,表示它必须经过网关登录校验。
用户没有登录,就不应该看到自己的地址数据。
3.2.2.UserAddress 实体
@Data
@Schema(description = "用户地址实体类")
@TableName("user_address")
public class UserAddress extends BaseEntity {
private Long userId;
private String name;
private String phone;
private String tagName;
private String provinceCode;
private String cityCode;
private String districtCode;
private String address;
private String fullAddress;
private Integer isDefault;
}
字段里最容易忽略的是 fullAddress。
它不是“多余字段”,而是为了让订单页、地址列表页可以直接展示完整地址,而不用每次再去拼省市区名称。
3.2.1.UserAddressController
@RestController
@RequestMapping("user/userAddress")
public class UserAddressController {
@Resource
private UserAddressService userAddressService;
//#获取用户地址列表
@GetMapping("auth/findUserAddressList")
public Result<List<UserAddress>> findUserAddressList() {
List<UserAddress> list = userAddressService.findListByCurrentUserId();
return Result.build(list, ResultCodeEnum.SUCCESS);
}
//#根据id获取用户地址详情
@GetMapping("auth/getById/{id}")
public Result<UserAddress> getById(@PathVariable Long id) {
UserAddress userAddress = userAddressService.getCurrentUserAddressById(id);
return Result.build(userAddress, ResultCodeEnum.SUCCESS);
}
}
3.2.2.UserAddressService
public interface UserAddressService extends IService<UserAddress> {
List<UserAddress> findListByCurrentUserId();
UserAddress getCurrentUserAddressById(Long id);
}
这里顺手把“按当前用户查询详情”的方法也先定义出来,后面编辑地址时直接复用。
3.2.3.UserAddressServiceImpl
@Service
public class UserAddressServiceImpl
extends ServiceImpl<UserAddressMapper, UserAddress>
implements UserAddressService {
@Override
public List<UserAddress> findListByCurrentUserId() {
Long userId = AuthThreadLocalUtils.getThreadLocal().getId();
return lambdaQuery()
.eq(UserAddress::getUserId, userId)
.orderByDesc(UserAddress::getIsDefault)
.orderByDesc(BaseEntity::getUpdateTime)
.list();
}
@Override
public UserAddress getCurrentUserAddressById(Long id) {
Long userId = AuthThreadLocalUtils.getThreadLocal().getId();
return lambdaQuery()
.eq(BaseEntity::getId, id)
.eq(UserAddress::getUserId, userId)
.one();
}
}
3.3.省市区三级联动
地址编辑页里最典型的交互就是“选省 -> 拉市 -> 拉区县”。
这就是三级联动。
它为什么要单独设计接口,而不是一次把全国所有地区都查出来?
- 全国地区数据量不小,一次性全量下发浪费带宽
- 前端真正需要的只是“当前节点的下一级”
- 后端按
parentCode查询,下拉框加载速度更稳定
2.3.1.接口说明
GET /api/user/region/findByParentCode/{parentCode}

2.3.2.Region 实体
@Data
@Schema(description = "区域实体类")
public class Region extends BaseEntity {
private String code;
private String parentCode;
private String name;
private Integer level;
}
2.3.3.RegionController
@RestController
@RequestMapping("user/region")
public class RegionController {
@Resource
private RegionService regionService;
@GetMapping("findByParentCode/{parentCode}")
public Result<List<Region>> findByParentCode(@PathVariable String parentCode) {
List<Region> list = regionService.findByParentCode(parentCode);
return Result.build(list, ResultCodeEnum.SUCCESS);
}
}
2.3.4.RegionService
public interface RegionService extends IService<Region> {
List<Region> findByParentCode(String parentCode);
}
2.3.5.RegionServiceImpl
@Service
public class RegionServiceImpl
extends ServiceImpl<RegionMapper, Region>
implements RegionService {
@Override
public List<Region> findByParentCode(String parentCode) {
return lambdaQuery()
.eq(Region::getParentCode, parentCode)
.orderByAsc(Region::getCode)
.list();
}
}
3.4.新增收货地址
新增地址真正难的不是“把一条记录插进去”,而是处理默认地址规则。
业务要求通常是:
- 用户至少要有一个默认地址
- 如果当前新增地址被设为默认,就要把旧默认地址取消
- 保存前要把省、市、区、详细地址拼成
fullAddress
2.4.1.接口说明
POST /api/user/userAddress/auth/save

2.4.2.UserAddressController
@PostMapping("auth/save")
public Result<String> saveAddress(@RequestBody UserAddress userAddress) {
String res = userAddressService.saveAddress(userAddress);
return Result.build(res, ResultCodeEnum.SUCCESS);
}
2.4.3.UserAddressService
public interface UserAddressService extends IService<UserAddress> {
List<UserAddress> findListByCurrentUserId();
UserAddress getCurrentUserAddressById(Long id);
String saveAddress(UserAddress userAddress);
String updateAddressById(UserAddress userAddress);
String removeAddressById(Long id);
}
2.4.4.UserAddressServiceImpl
@Service
public class UserAddressServiceImpl
extends ServiceImpl<UserAddressMapper, UserAddress>
implements UserAddressService {
@Resource
private RegionService regionService;
@Override
@Transactional(rollbackFor = Exception.class)
public String saveAddress(UserAddress userAddress) {
Long userId = AuthThreadLocalUtils.get().getId();
userAddress.setUserId(userId);
userAddress.setFullAddress(buildFullAddress(userAddress));
UserAddress defaultAddress = getDefaultAddress(userId);
if (defaultAddress == null) {
userAddress.setIsDefault(1);
save(userAddress);
return "新增成功";
}
if (Objects.equals(userAddress.getIsDefault(), 1)) {
clearDefaultAddress(userId, null);
} else {
userAddress.setIsDefault(0);
}
save(userAddress);
return "新增成功";
}
private UserAddress getDefaultAddress(Long userId) {
return lambdaQuery()
.eq(UserAddress::getUserId, userId)
.eq(UserAddress::getIsDefault, 1)
.one();
}
private void clearDefaultAddress(Long userId, Long excludeId) {
LambdaUpdateChainWrapper<UserAddress> wrapper = lambdaUpdate()
.set(UserAddress::getIsDefault, 0)
.eq(UserAddress::getUserId, userId);
if (excludeId != null) {
wrapper.ne(BaseEntity::getId, excludeId);
}
wrapper.update();
}
private String buildFullAddress(UserAddress userAddress) {
Region province = regionService.getByCode(userAddress.getProvinceCode());
Region city = regionService.getByCode(userAddress.getCityCode());
Region district = regionService.getByCode(userAddress.getDistrictCode());
return province.getName()
+ city.getName()
+ district.getName()
+ userAddress.getAddress();
}
}
上面这段逻辑可以按顺序理解:
- 从
ThreadLocal中拿到当前用户 - 根据地区编码查询省、市、区名称
- 组装出
fullAddress - 判断当前用户是否已有默认地址
- 没有默认地址,本次新增自动设为默认
- 有默认地址且本次勾选默认,就把旧默认取消
这样设计后,默认地址规则就不会失控。

3.5.修改收货地址
修改地址时需要同时保证三件事:
- 只能修改当前用户自己的地址
- 不能让用户最终一个默认地址都没有
- 如果把当前地址设为默认,要先取消其他默认地址
2.5.1.UserAddressController
@PutMapping("auth/updateById")
public Result<String> updateAddressById(@RequestBody UserAddress userAddress) {
String res = userAddressService.updateAddressById(userAddress);
return Result.build(res, ResultCodeEnum.SUCCESS);
}
2.5.2.UserAddressServiceImpl
@Override
@Transactional(rollbackFor = Exception.class)
public String updateAddressById(UserAddress userAddress) {
//1.获得用户id
Long userId = AuthThreadLocalUtils.get().getId();
//2.通过地址id查询 地址
UserAddress dbAddress = getCurrentUserAddressById(userAddress.getId());
if (dbAddress == null) {
throw new CustomException(ResultCodeEnum.ACCOUNT_STOP);
}
//3.如果取消默认地址,则判断该用户是否还有默认地址
if (Objects.equals(userAddress.getIsDefault(), 0)) {
long defaultCount = lambdaQuery()
.eq(UserAddress::getUserId, userId)
.eq(UserAddress::getIsDefault, 1)
.count();
if (defaultCount < 1) {
throw new CustomException(ResultCodeEnum.USER_ADDRESS_EXISTS);
}
}
//4.如果设置默认地址,则取消该用户其他地址的默认地址
if (Objects.equals(userAddress.getIsDefault(), 1)) {
clearDefaultAddress(userId, userAddress.getId());
}
userAddress.setUserId(userId);
userAddress.setFullAddress(buildFullAddress(userAddress));
updateById(userAddress);
return "更新成功";
}
3.6.删除收货地址
删除操作看起来很简单,但也要遵守业务规则。
最常见的约束是:
- 默认地址不允许直接删除
- 删除时同样要校验用户归属
2.6.1.UserAddressController
@DeleteMapping("auth/removeById/{id}")
public Result<String> removeAddressById(@PathVariable Long id) {
String res = userAddressService.removeAddressById(id);
return Result.build(res, ResultCodeEnum.SUCCESS);
}
2.6.2.UserAddressServiceImpl
@Override
public String removeAddressById(Long id) {
UserAddress userAddress = getCurrentUserAddressById(id);
if (userAddress == null) {
throw new CustomException(ResultCodeEnum.USER_ADDRESS_NOT_EXISTS);
}
if (Objects.equals(userAddress.getIsDefault(), 1)) {
throw new CustomException(ResultCodeEnum.USER_ADDRESS_EXISTS);
}
removeById(id);
return "删除成功";
}
3.7.Long 精度问题为什么会影响地址修改
这也是原笔记里提到但学生经常一知半解的地方。
问题本质是:
- Java 的
Long没问题 - 但前端
JavaScript的Number对超大整数精度有限 - 如果后端把
Long id直接按数字返回,前端拿到后可能发生精度丢失
一旦 id 变了,修改接口再把错误的 id 传回来,就会出现:
- 查不到记录
- 或者修改到错误数据
常见处理方式是在基础实体的 id 上统一加字符串序列化:
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
如果你的 zx-common 或 zx-model 已经统一处理过这一点,就不需要在每个实体里重复写。
4.收藏模块
收藏功能比购物车简单,但它非常适合练习“当前用户 + 业务表 + 远程调用”这三个点如何配合。
它的核心问题有两个:
- 当前用户是否收藏过这个商品
- 我的收藏页如何展示商品名称、图片、价格这些不在
user_collect表里的数据
前言
4.1.需求分析
先看页面行为:


用户在商品详情页可能发生 4 个动作:
- 点击收藏
- 再点一次取消收藏
- 打开详情页时判断当前商品是否已收藏
- 打开“我的收藏”分页查看全部收藏商品
4.2.收藏与取消收藏
4.2.1.接口设计说明
4.2.2.UserCollectController
@RestController
@RequestMapping("user/userinfo")
public class UserCollectController {
@Resource
private UserCollectService userCollectService;
@GetMapping("auth/collect/{skuId}")
public Result<String> collect(@PathVariable Long skuId) {
return userCollectService.collect(skuId);
}
@GetMapping("auth/cancelCollect/{skuId}")
public Result<String> cancelCollect(@PathVariable Long skuId) {
return userCollectService.cancelCollect(skuId);
}
@GetMapping("isCollect/{skuId}")
public Result<Boolean> isCollect(@PathVariable Long skuId) {
return userCollectService.isCollect(skuId);
}
}
4.2.2.UserCollectService
public interface UserCollectService extends IService<UserCollect> {
Result<String> collect(Long skuId);
Result<String> cancelCollect(Long skuId);
Result<Boolean> isCollect(Long skuId);
PageInfo<UserCollectVo> findUserCollectPage(Integer pageNum, Integer pageSize);
}
4.2.3.UserCollectServiceImpl
@Service
public class UserCollectServiceImpl
extends ServiceImpl<UserCollectMapper, UserCollect>
implements UserCollectService {
@Override
public Result<String> collect(Long skuId) {
Long userId = AuthThreadLocalUtils.get().getId();
UserCollect one = lambdaQuery()
.eq(UserCollect::getUserId, userId)
.eq(UserCollect::getSkuId, skuId)
.one();
if (one != null) {
return Result.build("已收藏", ResultCodeEnum.SUCCESS);
}
UserCollect collect = new UserCollect();
collect.setUserId(userId);
collect.setSkuId(skuId);
save(collect);
return Result.build("收藏成功", ResultCodeEnum.SUCCESS);
}
@Override
public Result<String> cancelCollect(Long skuId) {
Long userId = AuthThreadLocalUtils.get().getId();
LambdaQueryWrapper<UserCollect> wrapper = Wrappers.lambdaQuery(UserCollect.class)
.eq(UserCollect::getUserId, userId)
.eq(UserCollect::getSkuId, skuId);
remove(wrapper);
return Result.build("取消收藏成功", ResultCodeEnum.SUCCESS);
}
@Override
public Result<Boolean> isCollect(Long skuId) {
UserInfo userInfo = AuthThreadLocalUtils.get();
if (userInfo == null) {
return Result.build(false, ResultCodeEnum.SUCCESS);
}
long count = lambdaQuery()
.eq(UserCollect::getUserId, userInfo.getId())
.eq(UserCollect::getSkuId, skuId)
.count();
return Result.build(count > 0, ResultCodeEnum.SUCCESS);
}
}
4.3.我的收藏为什么必须远程调用商品服务
user_collect 表里只记录了两类信息:
- 谁收藏了
- 收藏了哪个
skuId
但收藏列表页真正要展示的是:
- 商品名称
- 商品主图
- 商品价格

这些都属于商品域数据,应该由 product-service 负责。
所以正确做法不是在 user-service 里直接跨表查商品表,而是:
- 先从
user_collect中查出当前页的skuId - 再远程调用商品服务批量拿商品信息
- 最后组装成收藏列表返回给前端

4.3.1.分页接口
@GetMapping("auth/findUserCollectPage/{pageNum}/{pageSize}")
public Result<PageInfo<UserCollectVo>> findUserCollectPage(@PathVariable Integer pageNum,
@PathVariable Integer pageSize) {
PageInfo<UserCollectVo> page = userCollectService.findUserCollectPage(pageNum, pageSize);
return Result.build(page, ResultCodeEnum.SUCCESS);
}
4.3.2.分页查询我的收藏
@Override
public PageInfo<UserCollectVo> findUserCollectPage(Integer pageNum, Integer pageSize) {
if (pageNum == null || pageSize == null) {
throw new CustomException(ResultCodeEnum.PARAM_ERROR);
}
Long userId = AuthThreadLocalUtils.getThreadLocal().getId();
Page<UserCollect> page = new Page<>(pageNum, pageSize);
Page<UserCollect> collectPage = lambdaQuery()
.eq(UserCollect::getUserId, userId)
.orderByDesc(BaseEntity::getCreateTime)
.page(page);
List<UserCollect> records = collectPage.getRecords();
if (records.isEmpty()) {
PageInfo<UserCollectVo> empty = new PageInfo<>(Collections.emptyList());
empty.setPageNum(pageNum);
empty.setPageSize(pageSize);
empty.setTotal(0);
empty.setPages(0);
return empty;
}
List<Long> skuIds = records.stream().map(UserCollect::getSkuId).toList();
//调用feign接口,批量查询商品信息
List<ProductSku> skuList =new ArrayList<>();
Map<Long, ProductSku> skuMap = skuList.stream()
.collect(Collectors.toMap(ProductSku::getId, sku -> sku));
List<UserCollectVo> voList = records.stream()
.map(record -> {
ProductSku sku = skuMap.get(record.getSkuId());
if (sku == null) {
return null;
}
UserCollectVo vo = new UserCollectVo();
vo.setSkuId(sku.getId());
vo.setSkuName(sku.getSkuName());
vo.setThumbImg(sku.getThumbImg());
vo.setSalePrice(sku.getSalePrice());
vo.setCreateTime(record.getCreateTime());
vo.setUpdateTime(record.getUpdateTime());
return vo;
})
.filter(Objects::nonNull)
.toList();
PageInfo<UserCollectVo> pageInfo = new PageInfo<>(voList);
pageInfo.setPageNum(pageNum);
pageInfo.setPageSize(pageSize);
pageInfo.setTotal(collectPage.getTotal());
pageInfo.setPages((int) collectPage.getPages());
return pageInfo;
}
4.3.1.商品服务提供批量查询接口
product-service:
public interface ProductSkuService extends IService<ProductSku> {
List<ProductSku> getSkuListBySkuIds(List<Long> ids);
}
@Service
public class ProductSkuServiceImpl
extends ServiceImpl<ProductSkuMapper, ProductSku>
implements ProductSkuService {
@Override
public List<ProductSku> getSkuListBySkuIds(List<Long> ids) {
return lambdaQuery()
.in(BaseEntity::getId, ids)
.list();
}
}
@RestController
@RequestMapping("productSku")
public class ProductSkuController {
@Resource
private ProductSkuService productSkuService;
@PostMapping("getSkuListBySkuIds")
public List<ProductSku> getSkuListBySkuIds(@RequestParam("ids") List<Long> ids) {
return productSkuService.getSkuListBySkuIds(ids);
}
}
4.3.2.用户服务定义 FeignClient
@FeignClient(value = "product-service")
public interface ProductSkuFeignClient {
@PostMapping("productSku/getSkuListBySkuIds")
List<ProductSku> getSkuListBySkuIds(@RequestParam("ids") List<Long> ids);
}
启动类别忘了开启:
@EnableFeignClients(basePackages = "com.zx.user.feignclient")