增加商城用户的相关功能

商城用户模块介绍

新蜂商城包含后台管理系统和商城端。在后台管理系统中可以对各个功能模块进行配置和操作,如轮播图管理、商品管理、订单管理等,对应的用户是管理员用户。管理员用户在后台管理系统的登录页面登录后,才能够在对应的页面进行操作。商城端则是用于完成整个购物流程的系统,该系统包含商城类系统的主要功能,如商品搜索、添加购物车、下单、支付等,对应的用户是商城用户。商城用户需要在商城端自行注册,登录之后就可以完成购物流程了。

新蜂商城有两种用户,分别是后台管理系统的管理员用户和商城端的商城用户,如图 6-1 所示。前面实战章节中讲解的都是后台管理系统相关的功能实现,如商品管理、轮播图管理等,代码中用到的用户是管理员用户。后续章节中将继续完善该实战项目,主要涉及商城端的功能模块,因此需要实现与商城用户相关的代码。

image 2025 04 23 15 02 18 039
Figure 1. 图6-1 新蜂商城用户

笔者的做法是将两种用户的相关编码实现都放在用户微服务中,编码是直接在 newbee-mall-cloud-user-service 模块中进行的。这是一种实现形式,如果有读者想把用户微服务拆分得更细一些,完全可以拆分为商城用户微服务和管理员用户微服务。

商城用户功能模块编码

本节的源代码是在 newbee-mall-cloud-dev-step09 工程的基础上改造的,名称为 newbee-mall-cloud-dev-step10。打开用户微服务 newbee-mall-cloud-user-web 的工程目录,将原单体 API 项目中与商城用户相关的业务代码和 Mapper 文件依次复制过来,如图 6-2 所示。

image 2025 04 23 15 03 11 623
Figure 2. 图6-2 原单体 API 项目中与商城用户相关的业务代码和 Mapper 文件

本步骤中的源代码涉及的数据库为 newbee_mall_cloud_user_db,数据库表为 tb_newbee_mall_user。

商城用户的表结构和建表语句如下:

USE 'newbee_mall_cloud_user_db';

# 创建商城用户表

CREATE TABLE 'tb_newbee_mall_user' (
  'user_id' bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户主键 id',
  'nick_name' varchar(50) NOT NULL DEFAULT '' COMMENT '用户昵称',
  'login_name' varchar(11) NOT NULL DEFAULT '' COMMENT '登录名称(默认为手机号)',
    'password_md5' varchar(32) NOT NULL DEFAULT '' COMMENT 'MD5加密后的密码',
    'introduce_sign' varchar(100) NOT NULL DEFAULT '' COMMENT '个性签名',
    'is_deleted' tinyint(4) NOT NULL DEFAULT '0' COMMENT '注销标识字段 (0-正常 1-已注销)',
    'locked_flag' tinyint(4) NOT NULL DEFAULT '0' COMMENT '锁定标识字段 (0-未锁定 1-已锁定)',
    'create_time' timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
    PRIMARY KEY ('user_id') USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

# 新增育城用户数据
INSERT INTO `tb_newbee_mall_user` (`user_id`, `nick_name`, `login_name`, `password_md5`, `introduce_sign`, `is_deleted`, `locked_flag`, `create_time`)
VALUES
(1, '十三', '13700002703', 'e10adc3949ba59abbe56e057f20f883e', '我怕千万人阻挡,只怕自己投降', 0, 0, '2022-05-22 08:44:57'),
(6, '陈尼克', '13711113333', 'e10adc3949ba59abbe56e057f20f883e', '测试用户陈尼克', 0, 0, '2022-05-22 08:44:57');

除此之外,与用户微服务相关的还有 Redis 数据库。

不过,仅仅复制过来是不够的,还需要对代码进行一些微调。有些工具类已经被移到公用模块 newbee-mall-cloud-common 中,代码中使用这些工具类的地方也需要处理一下引用路径。Controller 类中的接口地址都做了微调,与之前单体项目中定义的 URL 略有不同。

商城用户模块代码完善

商城用户的鉴权实现逻辑也要修改。与管理员用户的处理方式类似,需要对微服务架构下用户鉴权的编码进行改造。实现思路与处理管理员用户的实现思路一致,将原来存储在 token 表中的数据改为存储在 Redis 数据库中,这样在网关层就可以通过读取 Redis 数据库中的数据实现商城用户的鉴权操作。

由于用户微服务模块中已经引入了 Redis 组件,也配置了相关内容,因此在改造商城用户相关逻辑实现时,只处理代码即可。

  1. 修改登录和退出登录方法

    原来的登录逻辑中会生成 MallUserToken 的相关数据并被保存到 MySQL 数据库中,现在把这部分数据保存到 Redis 数据库中,修改后的登录方法代码如下:

    public String login(String loginName, String passwordMD5) {
        MallUser mallUser = mallUserMapper.selectByLoginNameAndPassword(loginName,
                passwordMD5);
        if (user != null) {
            if (user.getLockedFlag() == 1) {
                return ServiceResultEnum.LOGIN_USER_LOCKED_ERROR.getResult();
            }
            //登录成功后修改 token 的值
            String token = getNewToken(System.currentTimeMillis() + "", user.getUserId());
            MallUserToken mallUserToken = new MallUserToken();
            mallUserToken.setToken(token);
            mallUserToken.setUserId(user.getUserId());
            ValueOperations<String, MallUserToken> setToken = redisTemplate.opsForValue();
            setToken.set(token, mallUserToken, 7 * 24 * 60 * 60, TimeUnit.SECONDS);
            //过期时间为 7 天
            return token;
        }
        return ServiceResultEnum.LOGIN_ERROR.getResult();
    }

    商城用户不仅有登录方法,还有退出登录方法,所以还要对 logout() 方法进行修改,调整后的代码如下:

    public Boolean logout(String token) {
        redisTemplate.delete(token);
        return true;
    }

    修改的类是 newbee-mall-cloud-user-web 模块下的 ltd.user.cloud.newbee.service.impl.MallUserServiceImpl 类。

    在 login() 方法中,删除原有保存和修改 MallUserToken 数据到 MySQL 数据库中的逻辑代码,之后使用 RedisTemplate 对象将 MallUserToken 数据存储到 Redis 数据库中,存储时的 key 为登录成功后生成的 token 值。

    在 logout() 方法中,删除操作 MySQL 数据库的逻辑代码,实现方式改为使用 RedisTemplate 对象将 Redis 数据库中对应的数据删除,使用的 key 为当前的 token 值。

  2. 修改商城用户 token 值处理的逻辑

    修改完登录方法的逻辑后,还要修改对应的鉴权逻辑。原来的逻辑是获取请求头中的 token 值,之后根据这个值查询 MySQL 数据库是否存在及是否过期。现在不需要查询 MySQL 数据库了,直接读取 Redis 数据库中是否有对应的 token 值即可,修改后的商城用户的鉴权代码如下:

    @Component
    public class TokenToMallUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Autowired
        private MallUserMapper mallUserMapper;
    
        public TokenToMallUserMethodArgumentResolver() {
        }
    
        public boolean supportsParameter(MethodParameter parameter) {
            if (parameter.hasParameterAnnotation(TokenToMallUser.class)) {
                return true;
            }
            return false;
        }
    
        public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
            if (parameter.getParameterAnnotation(TokenToMallUser.class) instanceof TokenToMallUser) {
                String token = webRequest.getHeader("token");
                if (null != token && !"".equals(token) && token.length() == 32) {
                    ValueOperations<String, MallUserToken> opsForMallUserToken = redisTemplate.opsForValue();
                    MallUserToken mallUserToken = opsForMallUserToken.get(token);
                    if (mallUserToken == null) {
                        throw NewBeeMallException.fail(ServiceResultEnum.TOKEN_EXPIRE_ERROR.getResult());
                    }
                    MallUser mallUser = mallUserMapper.selectByPrimaryKey(mallUserToken.getUserId());
                    if (mallUser == null) {
                        throw NewBeeMallException.fail(ServiceResultEnum.USER_NULL.getResult());
                    }
    
                    if (mallUser.getLockedFlag().intValue() == 1) {
                        NewBeeMallException.fail(ServiceResultEnum.LOGIN_USER_LOCKED_ERROR.getResult());
                    }
                    return mallUserToken;
                } else {
                    NewBeeMallException.fail(ServiceResultEnum.NOT_LOGIN_ERROR.getResult());
                }
                return null;
            }
        }
    }

    修改的类是 newbee-mall-cloud-user-web 模块下的 ltd.user.cloud.newbee.service.config.handler.TokenToMallUserMethodArgumentResolver 类。

    删除原有查询 MySQL 数据库中 MallUserToken 数据的逻辑代码,之后添加从 Redis 数据库中读取 MallUserToken 数据的代码,后续逻辑并没有更改。

    接下来在 WebMVC 配置类中增加对 TokenToMallUserMethodArgumentResolver 的配置并使其生效。增加商城用户功能模块后,WebMVC 配置类就不只是处理管理员用户了,所以将该类的名称由 AdminUserWebMvcConfigurer 改成 UserServiceWebMvcConfigurer。最终的代码如下:

    public class UserServiceWebMvcConfigurer extends WebMvcConfigurationSupport {
    
        @Autowired
        private TokenToAdminUserMethodArgumentResolver tokenToAdminUserMethodArgumentResolver;
    
        @Autowired
        private TokenToMallUserMethodArgumentResolver tokenToMallUserMethodArgumentResolver;
    
        /**
         * @param argumentResolvers
         * @tip @TokenToAdminUser 注解处理方法
         */
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
            argumentResolvers.add(tokenToAdminUserMethodArgumentResolver);
            argumentResolvers.add(tokenToMallUserMethodArgumentResolver);
        }
    
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.
                    addResourceHandler("/swagger-ui/**")
                    .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/")
                    .resourceChain(false);
        }
    }

OpenFeign 编码暴露远程接口

在暴露远程接口前,将需要暴露的接口代码编写完成。笔者在 NewBeeMallCloudPersonalController 类中新建了一个根据 token 值查询商城用户信息的接口,并将其暴露,代码如下:

@RequestMapping(value = "/getDetailByToken", method = RequestMethod.GET)
public Result getMallUserByToken(@RequestParam("token") String token) {
    MallUser userDetailByToken =
            newBeeMallUserService.getUserDetailByToken(token);
    if (userDetailByToken != null) {
        Result result = ResultGenerator.genSuccessResult();
        result.setData(userDetailByToken);
        return result;
    }
    return ResultGenerator.genFailResult("无此用户数据");
}

该方法对应的业务层方法实现代码如下:

public MallUser getUserDetailByToken(String token) {
    ValueOperations<String, MallUser> opsForMallUserToken =
        redisTemplate.opsForValue();

    MallUserToken mallUserToken = opsForMallUserToken.get(token);
    if (mallUserToken != null) {
        MallUser mallUser = mallUserMapper.selectByPrimaryKey(mallUserToken.getUserId());
        if (mallUser == null) {
            NewBeeMallException.fail(ServiceResultEnum.DATA_NOT_EXIST.getResult());
        }

        if (mallUser.getLockedFlag().intValue() == 1) {
            NewBeeMallException.fail(ServiceResultEnum.LOGIN_USER_LOCKED_ERROR.getResult());
        }
        return mallUser;
    }
    NewBeeMallException.fail(ServiceResultEnum.DATA_NOT_EXIST.getResult());
    return null;
}

代码逻辑如下。

  1. 根据 token 值查询 Redis 数据库中对应的 MallUserToken 对象。

  2. 如果不为空,则根据获取的 MallUserToken 对象中的 userId 字段查询 MySQL 数据库中的商城用户信息;如果为空,则返回错误提示。

  3. 如果查询到正确的商城用户信息,则返回给调用端。

接下来,在 newbee-mall-cloud-user-api 模块中新增商城用户需要暴露的接口 FeignClient,后续在改造商城端的功能模块时需要用到。

之前管理员用户中的远程接口配置在 ltd.user.cloud.newbee.openfeign.NewBeeCloudAdminUserServiceFeign 类中,增加商城用户功能模块后,NewBeeCloudAdminUserServiceFeign 类就不只处理管理员用户了,因此将该类的名称由 NewBeeCloudAdminUserServiceFeign 改为 NewBeeCloudUserServiceFeign。注意,修改类名后,引用了该类的代码都需要同步改动。新增对 users/mall/getDetailByToken 接口的配置,最终的代码如下:

package ltd.user.cloud.newbee.openfeign;

import ltd.user.cloud.newbee.dto.MallUserDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import ltd.common.cloud.newbee.dto.Result;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "newbee-mall-cloud-user-service", path = "/users")
public interface NewBeeCloudUserServiceFeign {

    @GetMapping(value = "/admin/{token}")
    Result getAdminUserByToken(@PathVariable(value = "token") String token);

    @GetMapping(value = "/mall/getDetailByToken")
    Result<MallUserDTO> getMallUserByToken(@RequestParam(value = "token") String token);
}

这个方法的作用就是根据 token 值获取商城用户的信息,响应的实体结构为 MallUserDTO,其中的字段都是商城用户的属性。调用的接口是 newbee-mall-cloud-user-web 模块下 NewBeeMallCloudPersonalController 类中的 getMallUserByToken() 方法。这样,如果其他服务需要通过 token 值获取商城用户数据,就可以引入 newbee-mall-cloud-user-api 模块作为依赖,并且直接调用 NewBeeCloudUserServiceFeign 类中的 getMallUserByToken() 方法。

商城用户鉴权功能测试

修改完代码后,测试步骤是不能漏掉的,一定要验证项目是否能正常启动、接口是否能正常调用,防止在代码移动过程中出现一些小问题,导致项目无法启动或代码报错。在项目启动前需要分别启动 Nacos Server 和 Redis Server,之后依次启动 newbee-mall-cloud-user-web 工程、newbee-mall-cloud-gateway-admin 工程下的主类。启动成功后,就可以进行本节的功能测试了。

打开用户微服务的 Swagger 页面,在浏览器中输入如下网址: http://localhost:29000/swagger-ui/index.html。

加入商城用户后的用户微服务接口文档如图 6-3 所示,页面中包括管理员用户的接口文档和商城用户的接口文档,本节在测试时使用的是与商城用户相关的接口。

image 2025 04 23 15 35 14 120
Figure 3. 图6-3 加入商城用户后的用户微服务接口文档
  1. 商城用户登录功能测试

    依次单击 “登录接口”、“Try it out” 按钮,在参数栏中输入账号、密码字段,在这里笔者使用建表时的测试账号进行测试,如图 6-4 所示。

    image 2025 04 23 15 36 10 031
    Figure 4. 图6-4 商城用户登录接口的测试过程

    当然,也可以使用商城用户的注册接口自行注册一个商城账号用于后续的功能测试。单击 “Execute” 按钮,接口的测试结果如图 6-5 所示。

    image 2025 04 23 15 36 38 664
    Figure 5. 图6-5 商城用户登录接口的测试结果

    接口测试成功,使用登录接口获取的 token 值可以用于后续关于商城端的功能测试。比如,笔者在测试时获取了一个值为 “97a89ec463f28d213ca23c1707b43e95” 的 token 字段。

    由于对 token 字段处理的逻辑做了改动,因此还需要查看 Redis 数据库中是否存在刚刚获取的 token 值,如图 6-6 所示。

    image 2025 04 23 15 37 22 065
    Figure 6. 图6-6 Redis 数据库中的用户登录数据

    此时,可以查看 Redis 数据库中索引为 13 的数据。token 值被正确地存入 Redis 数据库,并且设置了过期时间。

  2. 获取商城用户信息接口测试

    接下来测试其他需要进行鉴权的接口,确认 token 字段处理的逻辑在改动后功能是否正常。

    依次单击 “获取用户信息”、“Try it out” 按钮,在登录认证 token 的输入框中输入管理员用户登录接口返回的 token 值,如图 6-7 所示。

    image 2025 04 23 15 38 29 786
    Figure 7. 图6-7 获取商城用户信息接口的测试过程

    单击 “Execute” 按钮,接口的测试结果如图 6-8 所示。

    image 2025 04 23 15 39 22 366
    Figure 8. 图6-8 获取商城用户信息接口的测试结果

    若后端接口的测试结果中有 “SUCCESS”,则表示信息获取成功。该接口对应到实际的项目页面中,是新蜂商城系统中的个人信息页面,如图 6-9 所示。

    image 2025 04 23 15 40 01 239
    Figure 9. 图6-9 新蜂商城系统中的个人信息页面

笔者在这里只演示了一个接口的测试过程,读者也可以测试其他接口。