用户中心系统(纯细节笔记,从0到1搭建项目完整流程)

项目简介

企业核心的用户中心系统,基于Spring Boot后端+React前端的全栈项目,实现了用户注册、登录、查询等基础功能

项目效果展示

登录界面:

注册界面:

欢迎页:

管理员界面:

普通用户界面:

简单调用模版组件:

企业做项目流程

需求分析 => 设计(概要设计、详细设计)=> 技术选型(充分调研) => 初始化 / 引入需要的技术 => 写 Demo => 写代码(实现业务逻辑) => 测试(单元测试、系统测试)=> 代码提交 / 代码评审 => 部署 => 发布上线
1.1. 需求分析  : 产品提出的需求
1.2. 首先需要阅读产品的需求文档并明确需求
1.3. 写一份需求文档并与团队同学确认
1.4. 设计(概要设计、详细设计)  :
1.5. 画出核心业务架构图、写出流程
1.6. 数据库设计
1.7. 功能模块详细设计
1.8. 技术选型  : 用什么技术来做这个项目
1.9. 选择什么编程语言、框架技术等。
1.10. 如果对不熟悉的技术需要进行调研
1.11. 初始化 / 引入需要的技术
1.12. 写 Demo  : 比如按照官方文档引入一个小组件
1.13. 写代码(实现业务逻辑)
1.14. 测试(单元测试、系统测试)  : 对自己写的增删改查代码进行测试
1.15. 代码提交 / 代码评审
1.16. 部署
1.17. 发布上线

需求分析

1. 登录、注册
2. 权限管理(对用户增删改查仅管理员可见
3. 用户校验(仅VIP用户)

技术选型

前端:

技术选型

  • HTML+CSS+JavaScript 三件套
  • React 开发框架
  • 组件库 Ant Design
  • Umi 开发框架
  • Umi Request 开发框架
  • Ant Design Pro(现成的管理系统)
后端:

技术选型

  • java核心编程
  • SSM框架(MVC模式)
  • spring(依赖注入框架,帮助管理 Java 对象)
  • springmvc(web 框架,提供接口访问、restful接口等能力)
  • mybatis(Java 操作数据库的框架,持久层框架,对 jdbc 的封装)
  • mybatis-plus(对 mybatis 的功能增强,不用写 sql 也能实现增删改查)
  • springboot(快速启动 / 集成项目,不用自己管理 spring 配置,不用自己整合各种框架)
  • junit 单元测试库
  • mysql 数据库
部署:云服务器 / docker容器 / 宝塔面板

初始化项目

前端初始化:

使用Ant.Design.Pro前端框架:Ant.Design.Pro官网,推荐用node版本v16.20.0,可使用nvm管理node版本

1、使用npm下载到本地

npm i @ant-design/pro-cli -g

pro create user-center-fronted

2、选择umi的版本

? 🐂 使用 umi@4 还是 umi@3 ? (Use arrow keys)

❯ umi@4

umi@3

? 🚀 要全量的还是一个简单的脚手架? (Use arrow keys)

❯ simple

complete

3、安装依赖

$ cd myapp && tyarn

// 或

$ cd myapp && npm install

4、启动(package.json文件中)

npm run start

文件结构介绍

├── config # umi 配置,包含路由,构建等配置

├── mock # 本地模拟数据

├── public

│ └── favicon.png # Favicon

├── src

│ ├── assets # 本地静态资源

│ ├── components # 业务通用组件

│ ├── e2e # 集成测试用例

│ ├── layouts # 通用布局

│ ├── models # 全局 dva model

│ ├── pages # 业务页面入口和常用模板

│ ├── services # 后台接口服务

│ ├── utils # 工具库

│ ├── locales # 国际化资源

│ ├── global.less # 全局样式

│ └── global.ts # 全局 JS

├── tests # 测试工具

├── README.md

└── package.json

规范的项目代码结构

src

├── components

└── pages

├── Welcome // 路由组件下不应该再包含其他路由组件,基于这个约定就能清楚的区分路由组件和非路由组件了

| ├── components // 对于复杂的页面可以再自己做更深层次的组织,但建议不要超过三层

| ├── Form.tsx

| ├── index.tsx // 页面组件的代码

| └── index.less // 页面样式

├── Order // 路由组件下不应该再包含其他路由组件,基于这个约定就能清楚的区分路由组件和非路由组件了

| ├── index.tsx

| └── index.less

├── User

| ├── components // group 下公用的组件集合

| ├── Login // group 下的页面 Login

| ├── Register // group 下的页面 Register

| └── util.ts // 这里可以有一些共用方法之类,不做推荐和约束,看业务场景自行做组织

└── * // 其它页面组件代码

后端初始化:

创建规范的后端目录
  1. controller : 请求层
  2. service :写业务逻辑
  3. model:数据库模型
  4. utils:工具
写代码流程
  1. 先做好业务设计流程
  2. 代码实现
  3. 持续优化(复用代码,使用设计模式,提取公共逻辑,提取枚举值等)
1.1. GitHub 搜现成的代码
1.2. SpringBoot 官方的模板生成器(https://start.spring.io/)
1.3. 直接在 IDEA 开发工具中生成  (一般都是自己创建工程)
1.4. 如果要引入 java 的包,可以去 maven 中心仓库寻找(http://mvnrepository.com/)
1.5. 新建好工程,然后整合 MyBatis-Plus 跟着官网来就可以了
1.6. 最好有自己的一套万能后端模版
2、连接数据库:Idea自带的数据库连接方式连接
3、配置数据库
spring:
application:
name: user-center
#DataSourse Config
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/yupi
username: root
password: 12345678
4、集成mybatis-plus
官网:MyBatis-Plus
1)引入依赖
         <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
2)加上注解

首先要创建一个Mapper文件夹,写一个对应数据表的接口集成mybatis-plus

public interface UserMapper extends BaseMapper<User> {
}

在入口类中打上注解(userCenterApplication)

@MapperScan("com.xhl.usercenter.Mapper")

3)写一个测试类

引入 junit 的依赖

<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>
@SpringBootTest
@RunWith(SpringRunner.class)
public class SampleTest {

    @Resource
    private UserMapper userMapper;

    @Test
    public void testSelect() {
        System.out.println(("----- selectAll method test ------"));
        List<User> userList = userMapper.selectList(null);

        Assert.assertEquals(5, userList.size());
        userList.forEach(System.out::println);
    }

}

设计数据库表:

1.1. 字段选择:确定每个用户表需要存储的信息。通常包括用户名、密码、电子邮件、电话号码、注册日期等基本信息。您可以根据需要增加其他自定义字段,如性别、年龄等。
1.2. 字段类型:为每个字段选择适当的数据类型。例如,用户名可以使用字符型(varchar)来存储,密码可以使用哈希值存储,电子邮件可以使用字符型等。
1.3. 主键设计:确定唯一标识每个用户的字段。通常使用自增整数或全局唯一标识符(GUID)作为主键。
1.4. 索引设计:根据数据查询的需求,选择合适的字段建立索引。例如,如果经常根据用户名查询用户信息,可以为用户名字段创建索引,提高查询效率。
1.5. 外键关联:如果用户表需要与其他表进行关联,例如用户表与订单表关联,可以设计外键将用户与其订单关联起来。
1.6. 完整性约束:定义字段的约束条件,确保存储的数据满足预期的规则和逻辑。例如,电子邮件字段可以添加格式验证约束,保证输入有效的电子邮件地址。
1.7. 表关系:如果需要进行多表操作和数据关联,可以使用关系型数据库的概念,如一对一、一对多、多对多等来设计表之间的关系。
用户表的设计

驼峰命名还是下划线命名根据公司规范来决定即可。可以开启mybatis-plus一键转换的。

  • id:用户唯一标识,类型为 bigint,自增长,作为主键。(默认添加了索引
  • userName:用户昵称,类型为 varchar(256),可以为空。
  • userAccount:用户账号,类型为 varchar(256),可以为空。
  • avatarUrl:用户头像 URL,类型为 varchar(1024),可以为空。
  • gender:用户性别,类型为 tinyint,可以为空。
  • userPassword:用户密码,类型为 varchar(512),不能为空。
  • phone:用户电话,类型为 varchar(128),可以为空。
  • email:用户邮箱,类型为 varchar(512),可以为空。
  • userStatus:用户状态,类型为 int,默认值为 0,表示正常。
  • createTime:创建时间,类型为 datetime,默认值为 CURRENT_TIMESTAMP,表示当前时间。
  • updateTime:更新时间,类型为 datetime,默认值为 CURRENT_TIMESTAMP,表示当前时间,会在数据更新时自动更新。
  • isDelete:是否删除,类型为 tinyint,默认值为 0,表示未删除。
  • userRole:用户角色,类型为 int,默认值为 0,表示普通用户,1 表示管理员。
    从表结构来看,该数据库设计的思路是建立一个用户表,用于存储用户的基本信息,包括账号、密码、昵称、头像、性别、电话、邮箱、状态、创建时间、更新时间、是否删除、用户角色等信息。这样可以方便地管理和维护用户信息,并且支持不同角色的用户权限控制。
create table user
(
    username     varchar(256)                       null comment '用户昵称',
    id           bigint auto_increment comment 'id'
        primary key,
    userAccount  varchar(256)                       null comment '账号',
    avatarUrl    varchar(1024)                      null comment '用户头像',
    gender       tinyint                            null comment '性别',
    userPassword varchar(512)                       not null comment '密码',
    phone        varchar(128)                       null comment '电话',
    email        varchar(512)                       null comment '邮箱',
    userStatus   int      default 0                 not null comment '状态 0 - 正常',
    createTime   datetime default CURRENT_TIMESTAMP null comment '创建时间',
    updateTime   datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
    isDelete     tinyint  default 0                 not null comment '是否删除',
    userRole     int      default 0                 not null comment '用户角色 0 - 普通用户 1 - 管理员'

)
    comment '用户';
设计思路
  1. 用户信息的基本属性:该用户表包含了用户的基本属性,包括用户名、用户账号、用户密码、用户电话、用户邮箱、用户状态、用户角色等。这些属性可以用于对用户进行基本的身份验证和管理。
  2. 用户信息的补充属性:该用户表还包含了一些补充属性,例如用户昵称、用户头像、用户性别等。这些属性可以丰富用户的信息,提高用户体验,但也需要注意这些属性是否为必填项,以及如何保护用户的隐私。
  3. 用户状态的设计:该用户表设计了用户状态字段,可以表示用户的状态,例如是否被锁定、是否被禁用等。这样可以方便对用户进行管理和维护。
  4. 用户角色的设计:该用户表还设计了用户角色字段,可以表示用户的角色,例如普通用户、管理员、超级管理员等。这样可以方便对用户权限进行控制,实现更精细的权限控制。
  5. 时间戳的设计:该用户表设计了创建时间和更新时间两个字段,分别记录用户数据的创建时间和最后一次更新时间。这样可以方便对用户数据的管理和维护。
实现数据库基本操作(使用自动生成器)
MyBatisX 插件,自动根据数据库⽣成
  • domain 实体对象
  • mapper(操作数据库的对象)
  • mapper.xml(定义了 mapper对象和数据库的关联,可以自己在里面写 SQL)
  • service(包含常用的增删改查)
  • serviceImpl(具体实现 service)

生成后搬运到自己的目录下面即可

让UserMapper.xml与自己的文件夹对应上

写一个测试类
  1. 在这里alt+enter直接生成一个测试类

  1. 把需要用到的注解先引入:@SpringBootTest @Resource @Test

  1. 下载一个插件:GenerateAllSetter

  2. 删去本来就有默认值的字段
  3. mybatis-plus默认会将驼峰转换成下划线,所以就出现 在“字段列表”中出现未知列“user_account”,在application.yml关闭默认转换
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: false
  1. 加一个断言并运行测试代码
@Test
    public void testAddUser() {
        User user = new User();
        user.setUsername("xhl001");
        user.setUserAccount("123456");
        user.setAvatarUrl("http.....");//网上找个图
        user.setGender(0);
        user.setUserPassword("");
        user.setPhone("123");
        user.setEmail("456");


        boolean result = userService.save(user);
        System.out.println(user.getId());
        //加一个断言
        assertTrue(result);
    }

注册模块

接口设计:

接受参数:用户账户、密码、确认密码
请求类型:POST
请求体:JSON 格式的数据
请求参数很长时不建议用 get
返回值:用户的ID

设计逻辑:

1. 用户在前端输入账户和密码、以及校验码(todo校验码表示可以先不做后期补充)
2. 校验用户的账户、密码、校验密码,是否符合要求
  • 非空
  • 账户长度 不小于 4 位
  • 密码 不小于 6 位
  • 账户不能重复
  • 账户不包含特殊字符
  • 密码和校验密码相同
3. 对密码进行加密(密码千万不要直接以明文存储到数据库中)
4. 向数据库插入用户数据

引入依赖

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>
  • Apache Commons Lang3 是 Apache 软件基金会开发的一组 Java 工具类库,提供各种常用字符串处理、
    数学运算、数据转换、校验等常用功能实现的工具类  。
    参考教程:https://www.cnblogs.com/niunafei/p/12812711.html
  • Java 中的盐值和密码混淆都是用来提高密码安全性的防护措施。
    盐值是一种用于增加密码破解难度的技术,在存储用户密码时,会将用户密码和一个随机生成的字符串(称为盐)进行组合,然后再进行加密存储。
    每个用户的盐值都是随机生成的,这样可以防止攻击者使用相同的方式对一组用户的密码执行攻击。
    在验证用户登录时,系统会使用相同的盐和用户输入的密码进行组合,并与存储的加密密码进行比对,以验证密码是否正确。

写代码

1. 在service里创建一个注册的接口,先确定好注册的参数与返回的类型

2. 在service/impl里面实现方法,安装一个好用的工具库
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>
根据设计逻辑写代码
a.判断账户特殊字符
 String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+ | {}【】‘;:”“’。,、?]";
        Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
        if (matcher.find()) {
            return -1;
        }
  • 定义正则表达式:首先定义一个字符串 validPattern,表示需要过滤的特殊字符,包括英文符号、中文符号、空格等。
  • 创建 Matcher 对象:使用 Pattern.compile(validPattern).matcher(userAccount) 方法,将正则表达式编译为一个 Matcher 对象,用于匹配用户账户中是否包含特殊字符。
  • 匹配用户账户:使用 matcher.find() 方法,对用户账户进行匹配,如果账户中包含任意一个特殊字符,则返回 -1,表示账户不合法,否则继续执行后续代码。
b.判断账户不能重复
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", userAccount);
        long count = userMapper.selectCount(queryWrapper);
        if (count > 1){
            return -1;
        }
  • 创建 QueryWrapper 对象:首先创建了一个 QueryWrapper 对象 queryWrapper,用于构建查询条件。
  • 添加查询条件:使用 eq 方法将用户账户 userAccount 添加到查询条件中,表示只查询账户 userAccount 相等的用户。
  • 添加查询条件:使用 eq 方法将用户账户 userAccount 添加到查询条件中,表示只查询账户 userAccount 相等的.
  • 判断用户数量:判断 count 是否大于 1,如果大于 1,则表示存在多个账户 userAccount 相等的用户,返回 -1 表示账户重复,否则继续执行后续代码。
c.对密码进行加密
 //加密
        final String SALT = "xhl";
        String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
  • 定义盐值:首先定义了一个字符串 SALT,用于增加密码的复杂度,提高安全性。
  • 使用 MD5 算法进行加密:使用 DigestUtils.md5DigestAsHex 方法,将 SALT 和用户密码 userPassword 拼接起来,然后使用 MD5 算法进行加密处理,得到加密后的密码 encryptPassword。
  • 返回加密后的密码:将加密后的密码 encryptPassword 返回给调用方,用于存储在数据库中或进行后续的验证操作。
d.向数据库插入数据
        User user = new User();
        user.setUserAccount(userAccount);
        user.setUserPassword(encryptPassword);
        boolean saveResult = this.save(user);
        if (!saveResult) {
            return -1;
        }
3. 统一前端参数类
@Data  //lombok注解 生成get set 方法
public class UserRegisterRequest implements Serializable {
    private static final long serialVersionUID = 3191241716373120793L;
    //请求参数
    private String userAccount;

    private String userPassword;

    private String checkPassword;

    private String planetCode;
}
4. 序列化的作用是什么?

将对象转换为字节流的过程,以便在网络上进行传输或将对象持久化到磁盘上。

在具体的应用场景中,序列化的作用包括:

  1. 网络传输:当需要通过网络将对象发送给其他机器或进程时,需要将对象序列化为字节流进行传输。接收方可以通过反序列化将字节流转换为对象,使得对象在网络中的传输成为可能。
  2. 持久化存储:将对象序列化为字节流后,可以将字节流保存到磁盘上,在下次启动时重新加载对象。这在应用程序重启后可以保留对象的状态,以实现数据的持久化存储。
  3. 分布式计算:在分布式系统或集群中,不同的节点需要共享对象数据。通过将对象序列化为字节流,可以在不同的节点之间传输和共享数据。

在你的注册请求类中,实现Serializable接口可以使该类的对象在网络传输或持久化存储时能够被序列化和反序列化。这对于注册请求对象的传输和保存都是非常有用的。

5. 测试一下

生成测试方法,测试插入一些数据是否成功

6. 完整代码
public long userRegister(String userAccount, String userPassword, String checkPassword) {
        if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
            return -1;
        }
        if (userAccount.length() < 4) {
            return -1;
        }
        if (userPassword.length() < 6 || checkPassword.length() < 6) {
            return -1;
        }
        //账户不能包含特殊字符
        String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+ | {}【】‘;:”“’。,、?]";
        Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
        if (matcher.find()) {
            return -1;
        }

        if (!userPassword.equals(checkPassword)) {
            return -1;
        }

        //判断账户不重复
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", userAccount);
        long count = userMapper.selectCount(queryWrapper);
        if (count > 0) {
            return -1;
        }

        //加密
        final String SALT = "xhl";
        String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());

        //插入数据
        User user = new User();
        user.setUserAccount(userAccount);
        user.setUserPassword(encryptPassword);
        boolean saveResult = this.save(user);
        if (!saveResult) {
            return -1;
        }
        return user.getId();


    }

登录模块

接口设计

接受参数:用户账户、密码
请求类型:POST
请求体:JSON 格式的数据
请求参数很长时不建议用 get
返回值:用户信息( 脱敏

设计逻辑

1.1. 校验用户账户和密码是否合法
    • 非空
    • 账户长度不小于 4 位
    • 密码就不小于 6 位
    • 账户不包含特殊字符
1.2. 校验密码是否输入正确,要和数据库中的密文密码去对比
1.3. 用户信息脱敏,隐藏敏感信息,防止数据库中的字段泄露
1.4. 我们要记录用户的登录态(session),将其存到服务器上(用后端 SpringBoot 框架封装的服务器 tomcat 去记录)
cookie
1.5. 返回脱敏后的用户信息

脱敏:信息脱敏(Data Masking)是一种隐私保护技术,通过对敏感数据进行修改或者替换的方式,来保护数据的隐私和安全。信息脱敏通常应用于需要处理敏感数据的场景,例如测试、开发、分析等环境。在信息脱敏技术中,被保护的敏感数据通常会被替换成某种规则定义的非敏感数据或者格式,以避免敏感数据泄露和数据窃取的风险,主要就是防止信息泄露,隐藏敏感信息

如何知道是哪个用户登录了?

Java Web中session与cookie的知识

1. 客户端连接服务器端后,得到一个 session 状态(匿名会话),返回给前端(用户已经有了会话,但是这个会在用户登录成功之后才会保存到Session)
2. 登录成功后,得到了登录成功的 session,并且给该session设置一些值(比如用户信息),返回给前端一个设置 cookie 的 命令:sessionID => cookie
3. 前端接收到后端的命令后,设置 cookie,保存到浏览器内
4. 前端再次请求后端的时候(相同的域名),在请求头中带上cookie去请求 (有sessionID)
5. 后端拿到前端传来的 cookie,找到对应的 session
6. 后端从 session 中可以取出基于该 session 存储的变量(用户的登录信息、登录名)

写代码

1. 在service里写接口
User userLogin(String userAccount, String userPassword, HttpServletRequest request);
2. 实现该接口,和刚刚实现注册接口的方法一致
2.1. 与数据库密码对比
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", userAccount);
        queryWrapper.eq("userPassword", encryptPassword);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            log.info("用户名或密码错误");
            return null;
        }

但是会查出来逻辑删除的用户,需要去mybatis-plus配置一下:

配置文件加上这段代码:

  global-config:
    db-config:
      logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

实体类字段上加上@TableLogic注解

@TableLogic
private Integer deleted;
2.2. 用户信息脱敏
  //用户信息脱敏
        User safeUser = new User();
        safeUser.setId(user.getId());
        safeUser.setUsername(user.getUsername());
        safeUser.setUserAccount(user.getUserAccount());
        safeUser.setAvatarUrl(user.getAvatarUrl());
        safeUser.setGender(user.getGender());
        safeUser.setPhone(user.getPhone());
        safeUser.setEmail(user.getEmail());
        safeUser.setUserStatus(user.getUserStatus());
        safeUser.setCreateTime(user.getCreateTime());
2.3. 记录用户登录状态

request.getSession().setAttribute(USER_LOGIN_STATE, user);

完整代码

public User userLogin(String userAccount, String userPassword, HttpServletRequest request) {
        if (StringUtils.isAnyBlank(userAccount, userPassword)){
            return null;
        }
        if (userAccount.length() < 4) {
            return null;
        }
        if (userPassword.length() < 6 ) {
            return null;
        }
        //账户不能包含特殊字符
        String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+ | {}【】‘;:”“’。,、?]";
        Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
        if (matcher.find()) {
            return null;
        }
        //密码对比
        String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", userAccount);
        queryWrapper.eq("userPassword", encryptPassword);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            log.info("用户名或密码错误");
            return null;
        }
        //用户信息脱敏
        User safeUser = new User();
        safeUser.setId(user.getId());
        safeUser.setUsername(user.getUsername());
        safeUser.setUserAccount(user.getUserAccount());
        safeUser.setAvatarUrl(user.getAvatarUrl());
        safeUser.setGender(user.getGender());
        safeUser.setPhone(user.getPhone());
        safeUser.setEmail(user.getEmail());
        safeUser.setUserStatus(user.getUserStatus());
        safeUser.setCreateTime(user.getCreateTime());

        //记录用户的登录状态
        request.getSession().setAttribute(USER_LOGIN_STATE, user);

        return user;

    }

注销登录

1. 在UserService里定义方法

int userLogout(HttpServletRequest request);
如果说注销成功的话,是不是要返回⼀个成功的标识,这⾥就先返回⼀个int类型。

2. 在UserServiceImpl实现方法:

当用户登录成功后,我们在这个请求体的这个session中,就是说这个请求对应的session中保存了⽤户的登录状态,之后,我们判断⽤户是否登录,只要看看这个session里面有没有这个标识,就能知道⽤户是否登录那同样我们如果想要⽤户注销的话,是不是反其道而行,⽤户登录需要设置⼀个登录状态,注销不就是移除⼀个登录状态,把同样的⼀个登录态的标识移除掉就可以了

public int userLogout(HttpServletRequest request) {
        request.getSession().removeAttribute(USER_LOGIN_STATE);
        return 1;
    }
3. 在controller里写接口
public Integer userLogout(HttpServletRequest request) {
        if(request==null){
            return null;
        }
        return userService.userLogout(request);
    }

封装成用户接口controller

@RestController适⽤于编写 restful 风格的 api,返回值默认为 json 类

先封装⼀个对象:专门用来来接收请求参数

需要用到java序列化的库:
封装前端请求需要Java序列化是因为在分布式系统中,数据需要在不同的计算机之间进行传输和共享。当数据需要在网络上传输时,需要将数据进行序列化以便于在网络上进行传输。Java序列化是将对象转换为字节流的过程,这样可以将对象在不同的计算机之间传输并在目标计算机上重新构造对象。
在这个例子中,UserRegisterRequest类实现了Serializable接口,这意味着该类的对象可以通过Java序列化转换为字节流,以便于在网络上进行传输。这样,当前端发送注册请求时,UserRegisterRequest对象将被序列化并在网络上进行传输,以便于后端服务器接收和处理注册请求。

注册

@Data
public class UserRegisterRequest implements Serializable {

    private static final long serialVersionUID = 3198657856785875L;

    private String userAccount;

    private String userPassword;

    private String checkPassword;

}
代码中的serialVersionUID的作用是为了确保在反序列化对象时,能够正确地匹配序列化版本,从而保证反序列化操作的正确性和一致性。

登录

@Data
public class UserLoginRequest implements Serializable {
    private static final long serialVersionUID = 3198657856785875L;

    private String userAccount;

    private String userPassword;


}

@RequestBody

打上⼀个注解@RequestBody,没打上的话,Springmvc框架不知道怎么把前端传来的json参数去
合适的对象做⼀个关联
在Spring框架中,@RequestBody注解用于将HTTP请求正文中的JSON、XML或其他数据绑定到方法的参数上。当我们使用该注解时,Spring会自动将请求正文中的数据反序列化成Java对象,并将其作为方法的参数传递。
具体来说,@RequestBody注解可以用于处理POST、PUT、DELETE等请求方法,并且可以指定请求数据的格式,比如JSON、XML等。使用该注解可以方便地将请求数据转换为Java对象,并进行相应的业务处理。

完整代码


public class UserController {

    @Resource
    private UserService userService;

    @PostMapping("/register")
    public Long userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
       if(userRegisterRequest==null){
           return null;
       }

       String userAccount = userRegisterRequest.getUserAccount();
       String userPassword = userRegisterRequest.getUserPassword();
       String checkPassword = userRegisterRequest.getCheckPassword();
       if(StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)){
           return null;
       }

        return userService.userRegister(userAccount, userPassword, checkPassword);

    }

    @PostMapping("/login")
    public User userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
        if(userLoginRequest==null){
            return null;
        }

        String userAccount = userLoginRequest.getUserAccount();
        String userPassword = userLoginRequest.getUserPassword();
        if(StringUtils.isAnyBlank(userAccount, userPassword)){
            return null;
        }

        return userService.userLogin(userAccount, userPassword,request);
    }
}

用户管理接口

必须验证权限:仅管理员可用,在数据库里加一个身份字段
1.1. 查询用户
1.2. 删除用户
用户脱敏

定义常量包

创建一个constant包专门用来保存常量,写一个用户接口
public interface UserConstant {

    /*
     * 用户登录状态
     */
    String USER_LOGIN_STATE = "userloginState";

    //权限
    /**
     * 默认权限
     */

    int DEFAULT_ROLE = 0;

    /**
     * 管理员权限
     */

    int ADMIN_ROLE = 1;
}

用户脱敏

把这个逻辑提取UserService的接口里: User getSafetyUser(User user);
之后要用到用户脱敏的话直接调用方法即可:

在 UserServiceImpl 类里实现
 @Override
    public User getSafetyUser(User user){
        //用户信息脱敏
        User safeUser = new User();
        safeUser.setId(user.getId());
        safeUser.setUsername(user.getUsername());
        safeUser.setUserAccount(user.getUserAccount());
        safeUser.setAvatarUrl(user.getAvatarUrl());
        safeUser.setGender(user.getGender());
        safeUser.setPhone(user.getPhone());
        safeUser.setEmail(user.getEmail());
        safeUser.setUserStatus(user.getUserStatus());
        safeUser.setCreateTime(user.getCreateTime());

        return safeUser;

    }

判断是否为管理员

Servlet的知识,获得到的是Object类型的session,需要强转类型
 private boolean isAdmin(HttpServletRequest request){
        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
        User user = (User) userObj;
        //判断是否为管理员
        return user != null && user.getUserRole() == ADMIN_ROLE;
    }

查询用户

在UserController类里写接口
这里用到了java8的知识:
1.1. Lambda表达式: user -> {...} 是一个Lambda表达式,它代表了一个函数式接口(在这里是Function<User, SafetyUser>),它接受一个User类型的参数并返回一个SafetyUser类型的结果。Lambda表达式是Java 8中引入的一种匿名函数的语法,它使得函数可以像其他数据类型一样被传递和使用。
1.2. Stream流:userList.stream()将List类型的userList转换成了一个流(Stream),这使得我们可以对这个列表进行各种操作,如过滤、映射、排序等。
1.3. map方法:map(user -> {...})是Stream API中的一个中间操作,它接收一个函数式接口作为参数,并将这个函数应用到流中的每个元素上。这里的函数式接口是Lambda表达式,它将每个User类型的对象映射为一个SafetyUser类型的对象。
1.4. collect方法:collect(Collectors.toList())是Stream API中的一个终止操作,它将流中的元素收集到一个列表中,并返回这个列表。在这个例子中,它将每个SafetyUser类型的对象收集到一个List中。
public List<User> searchUsers(String username,HttpServletRequest request){
        if(!isAdmin(request)){
            return new ArrayList<>();
        }

        QueryWrapper<User> queryWrapper=new QueryWrapper<>();
        if(StringUtils.isNotBlank(username)){
            queryWrapper.like("username",username);
        }
        List<User> userList =  userService.list(queryWrapper);

        //用户脱敏
         return  userList.stream().map(user ->
              userService.getSafetyUser(user)
        ).collect(Collectors.toList());

    }

删除用户

根据用户Id删除用户
 public boolean deleteUser(@RequestBody long id,HttpServletRequest request){

        if(!isAdmin(request)){
            return false;
        }
        if(id<=0){
            return  false;
        }
        return userService.removeById(id);
    }

获取当前登录用户的信息

 public User getCurrentUser(HttpServletRequest request) {
        //判断是否登录
        Object userobj = request.getAttribute(USER_LOGIN_STATE);
        User currentUser = (User) userobj;
        if (currentUser == null) {
            return null;
        }
        long userId = currentUser.getId();
        //todo:校验用户是否合法
        User user = userService.getById(userId);
        return userService.getSafetyUser(user);

    }
具体逻辑:
  1. getCurrentUser(HttpServletRequest request) 方法接收一个 HttpServletRequest 对象,该对象包含了当前 HTTP 请求的信息,其中包括登录用户的信息。
  2. Object userobj = request.getAttribute(USER_LOGIN_STATE)HttpServletRequest 中获取登录用户的信息,其中 USER_LOGIN_STATE 是一个常量,表示当前登录用户的属性名。
  3. User currentUser = (User) userobj 将获取到的用户信息强制转换为 User 类型,这里假设登录用户的信息是以 User 对象的形式存储在 HttpServletRequest 中的。
  4. if (currentUser == null) { return null; } 如果当前登录用户为 null,说明当前用户未登录,直接返回 null
  5. long userId = currentUser.getId() 从当前登录用户中获取用户 ID。
  6. User user = userService.getById(userId) 根据用户 ID 从数据库中查询用户信息。
  7. return userService.getSafetyUser(user) 将查询到的用户信息传入 userService.getSafetyUser(user) 方法中,该方法将返回一个经过安全处理的用户信息,例如对敏感信息进行脱敏、过滤掉不必要的字段等。

用户校验

在用户使用我们的系统时候,需要校验其身份是否为合法的用户(是否为vip有唯一的编号)
先让用户自己填:2 - 5 位编号,全凭自觉。
后台补充对编号的校验:长度校验、唯一性校验
前端补充输入框,适配后端。
后期拉取星球数据,定期清理违规用户

前端页面编写

首先打开项目,不带假数据启动:npm run dev

修改底部版权信息

const Footer: React.FC = () => {
  const defaultMessage = "苏御出品";
  const currentYear = new Date().getFullYear();
  return (
    <DefaultFooter
      copyright={`${currentYear} ${defaultMessage}`}
      style={{
        background: 'none',
      }}
      links={[
        {
          key: 'planet',
          title: '优质平台',
          href: SYSTEM_LINK,
          blankTarget: true,
        },
        {
          key: 'github',
          title: <><GithubOutlined /> 苏御 GitHub </>,
          href: 'https://github.com/Atopos-suyu',
          blankTarget: true,
        },
        {
          key: 'project',
          title: '用户中心管理',
          href: 'https://apps.youkeda.com/',
          blankTarget: true,
        },
      ]}
    />
  );
};

修改Logo

1. 创建constant常量包,添加一个Index.ts的文件
/**
 * 优课达logo图片参数
 */
export const SYSTEM_LOGO = "https://tse4-mm.cn.bing.net/th/id/OIP-C.yCNuKZQ8NXxRRtailFI5VQHaHa?rs=1&pid=ImgDetMain";

/**
 * 优课达学习网址
 */
export const SYSTEM_LINK = "https://apps.youkeda.com/";
2. 在user/index.tsx里将Logo换成自己引入的:
把 "/logo.svg" 改为 {SYSTEM_LOGO},这⾥要导⼊⼀下,根据提示或使⽤快捷键

3. 在Footer->index.tsx也可以直接使⽤ PLANET_LINK

删除多余的代码

  • 删掉手机号登录
  • 删掉 自动提示错误的用户名和密码改成 “请输入账号

  • 验证码和手机号登录的代码删掉

  • 设置忘记密码跳转的链接

页面显示:

对接请求

怎么发请求:前端使用 ajax 来请求后端

前端请求库及封装关系

  • axios 封装了 ajax
  • request 是 ant design 项目又封装了一次
1. 追踪 request 源码:用到了 umi 的插件、requestConfig 配置文件
2. 查询handleSubmit

3. 点击LoginParams

改成如下格式:

type LoginParams = {
  userAccount?: string;
  userPassword?: string;
  autoLogin?: boolean;
  type?: string;
};
4. 回到handleSubmit,点击login

5. 修改request的请求地址,这里可以去看官方文档:

升级到 V5 - Ant Design Pro

6. 提示我们要去app.ts去配置,顺便把官网提示的配置复制
import { RequestConfig } from 'umi';

export const request: RequestConfig = {
  prefix: 'http://localhost:8080/',
  timeout: 10000,

};

在api.ts中,把请求地址改成/user/login

跨域问题解决

当前端的浏览器里的地址,它是8000端口, 但是我们的后端,它是8080端口,端口不⼀样,就是跨域的。
跨域有很多种解决的方法,可以搭建一个代理,或者在后端支持跨域。
1.1. Ant Design Pro它提供了⼀个配置代理的方式,我们直接用它的代理即可。

1.2. 在请求里加上/api

1.3. 后台也需要加上统一的/api配置
server:
port: 8080
servlet:
context-path: /api

当我们访问本地地址8000+api前缀的话会自动代理带后端8080

发现如果请求会直接请求到8080,那么我们不写prefix即可成功:

代理

正向代理:替客户端向服务器发送请求,可以解决跨域问题

反向代理:用户去请求代理,代理服务器转发,替服务器统一接收请求。

怎么实现代理?

  • Nginx 服务器
  • Node.js 服务器

原本请求:http://localhost:8000/api/user/login

代理到请求:http://localhost:8080/api/user/login

把请求参数替换成和后端一致

将username换成userAccount

password替换成userPassword

给密码多加⼀个长度校验

测试一下

有响应信息了

前端框架介绍

Ant Design 组件库 => 基于 React 实现
Ant Design Procomponents => 基于 Ant Design 实现
Ant Design Pro 后台管理系统 => 基于 Ant Design + React + Ant Design Procomponents + 其他的库实现
MFSU:前端编译优化
1.1. app.tsx:项目全局入口文件,定义了整个项目中使用的公共数据(比如用户信息)
1.2. access.ts 控制用户的访问权限

获取初始状态流程:首次访问页面(刷新页面),进入 app.tsx,执行 getInitialState 方法,该方法的返回值就是全局可用的状态值。

完成注册界面开发

1. 增加注册页面
1.1. 直接复制登录页面,重命名为Register(复制整个Login⽂件夹,粘贴到user⽂件夹下,重命名)

1.2. 修改组件名称为Register

2. 定义注册的地址
2.1. 新建路由
在config中找到routes.ts文件,新建一个注册的路由

2.2. 在app.tsx中修改重定向页面,写一个允许页面的白名单
  onPageChange: () => {
      const { location } = history;
      //添加一个白名单
      const whiteList=['/user/register',loginPath];
      if(whiteList.includes(location.pathname)){
          return;
      }

      // 如果没有登录,重定向到 login
      if (!initialState?.currentUser) {
        history.push(loginPath);
      }
    },

访问http://localhost:8000/user/register 可以看到注册页面了

为什么会自动触发重定向到登录页面的逻辑?

因为app.tsx 它是咱们这个前端项⽬的全局⽂件,⾥⾯定义了刚进⼊⼀个页面时要执行的逻辑,⽐如说我们刚进⼊⻚⾯要获取⽤户信息,如果没获取到⽤户信息,就重定向到登录页让你登录,那同时,因为使⽤的这个框架,更偏向于后台管理系统,所以说每个⻚⾯都会去校验。如果说⽤户没登录,没有登录状态,那我们就把它重定向到登录页。

写前端的代码

1.1. 删除没用的代码
1.2. 将页面标题改为账号密码注册

1.3. 加上一个确认密码的的框,注意name属性要和后端的字段对应起来

1.4. 全局替换 :登录换成注册
发现登录按钮的文字没有变成注册,因为loginform组件是自带的,不是我们自己写的。
去看一下loginform的源码:index.js


在index.tsx中新增代码:
 


看看效果:

写提交请求的逻辑

1.1. 添加一个RegisterParams

1.2. 把 LoginParams 全部替换为 RegisterParams
1.3. 在提交前写一些校验:

1.4. 写一个注册的方法:Ctrl点击login


⾥有⼀个api,复制代码,粘贴到这段代码下⾯,修改⼀下


点击 LoginResult (这⾥定义了向后端发送请求之后,给前端返回什么类型的数据)
 


回到api.ts 把 LoginResult 改为 RegisterResult
回到index.tsx 把 user 改为 id , login 改为 register

1.5. 删掉没⽤的代码(这些是登录⻚⾯的错误提示,这里不需要),再加上一个注册成功后重定向到登录页面
完整代码:
import Footer from '@/components/Footer';
import { register } from '@/services/ant-design-pro/api';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { LoginForm, ProFormText } from '@ant-design/pro-components';
import { message, Tabs } from 'antd';
import React, { useState } from 'react';
import { history } from 'umi';
import styles from './index.less';
import { SYSTEM_LOGO } from '@/constant';

const Register: React.FC = () => {
  const [type, setType] = useState<string>('account');

  const handleSubmit = async (values: API.RegisterParams) => {
    const { userPassword, checkPassword } = values;
    //校验
    if (checkPassword !== userPassword) {
      message.error('两次密码不一致');
      return;
    }

    try {
      // 注册
      const id = await register({
        ...values,
        type,
      });
      if (id > 0) {
        const defaultLoginSuccessMessage = '注册成功!';
        message.success(defaultLoginSuccessMessage);
        /** 此方法会跳转到 redirect 参数所在的位置 */
        if (!history) return;
        const { query } = history.location;
        
        history.push({
          pathname: 'user/login',
          query
        })
        return;
      } else {
        throw new Error('register error id = ${id}');
      }
    } catch (error) {
      const defaultLoginFailureMessage = '注册失败,请重试!';
      message.error(defaultLoginFailureMessage);
    }
  };
  return (
    <div className={styles.container}>
      <div className={styles.content}>
        <LoginForm
          submitter={{
            searchConfig: {
              submitText: '注册',
            },
          }}
          logo={<img alt="logo" src={SYSTEM_LOGO} />}
          title="小火龙的用户中心"
          subTitle={'keep learning,keep writing,keep coding'}
          initialValues={{
            autoLogin: true,
          }}
          onFinish={async (values) => {
            await handleSubmit(values as API.RegisterParams);
          }}
        >
          <Tabs activeKey={type} onChange={setType}>
            <Tabs.TabPane key="account" tab={'账户密码注册'} />
          </Tabs>

          {type === 'account' && (
            <>
              <ProFormText
                name="userAccount"
                fieldProps={{
                  size: 'large',
                  prefix: <UserOutlined className={styles.prefixIcon} />,
                }}
                placeholder={'请输入账号'}
                rules={[
                  {
                    required: true,
                    message: '账号是必填项!',
                  },
                ]}
              />
              <ProFormText.Password
                name="userPassword"
                fieldProps={{
                  size: 'large',
                  prefix: <LockOutlined className={styles.prefixIcon} />,
                }}
                placeholder={'请输入密码'}
                rules={[
                  {
                    required: true,
                    message: '密码是必填项!',
                  },
                  {
                    min: 8,
                    type: 'string',
                    message: '密码长度不能小于8位',
                  },
                ]}
              />

              <ProFormText.Password
                name="checkPassword"
                fieldProps={{
                  size: 'large',
                  prefix: <LockOutlined className={styles.prefixIcon} />,
                }}
                placeholder={'请再次输入密码'}
                rules={[
                  {
                    required: true,
                    message: '确认密码是必填项!',
                  },
                  {
                    min: 8,
                    type: 'string',
                    message: '密码长度不能小于8位',
                  },
                ]}
              />
            </>
          )}
        </LoginForm>
      </div>
      <Footer />
    </div>
  );
};
export default Register;

测试一下

对接获取当前用户信息的接口

  • 回到前端,点击queryCurrentUser

  • 根据 数据库内的字段,决定返回的东⻄

  • 将魔法值提取为常量:

const NO_NEED_LOGIN_WHITE_LIST = ['/user/register', loginPath];

  • 修改一下判断的逻辑:

  • 改一下api的请求路径:

  • 测试登录成功:

为什么头像加载不出来?
找到AvatarDropdown.tsx修改⼀下头像的变量名,和用户名的变量名。

完成用户管理后台页面开发

创建用户管理页面
1.1. 在page⽂件夹下新建Admin⽂件夹,把user⽂件夹下的Register⽂件夹复制,粘贴到admin⽂件夹下,并改名为UserMange

1.2. 在route.ts添加⼀个路由

发现access:''canAdmin',也就是只有管理员才能访问,去access.ts里先改一下

再去访问发现成功了:

开发管理页面

1.1. 可以发现我们复制的是注册的代码,但是显示的有问题,问题出来:component: './Admin'

1.2. Admin.tsx内的⽂本内容和⻚⾯显示的相同,这说明这个⻚⾯已经写死了,修改⼀下内容
import { PageHeaderWrapper } from '@ant-design/pro-components';
import React from 'react';
const Admin: React.FC = (props) => {
  const { children } = props;
  return (
    <PageHeaderWrapper>
      {children}
    </PageHeaderWrapper>
  );
};
export default Admin;
1.3. 把自定义的组件传给admin组件来渲染

1.4. 去官方文档直接使用现有的表格:https://procomponents.ant.design/components/table?tab=api&current=1&pageSize=5

复制代码到index.tsx中:

1.5. 删去没用的代码,根据数据库的字段修改展示列

1.6. 在api.ts 增加⼀个搜索⽤户的接⼝

1.7. 回到UserManage⽂件夹下的index.tsx加上这段代码:

完整代码:
import React, { useRef } from 'react';
import type { ProColumns, ActionType } from '@ant-design/pro-table';
import ProTable, { TableDropdown } from '@ant-design/pro-table';
import { searchUsers } from '@/services/ant-design-pro/api';
import { Image } from 'antd';

const columns: ProColumns<API.CurrentUser>[] = [
  {
    dataIndex: 'id',
    valueType: 'indexBorder',
    width: 48,
  },
  {
    title: '用户名',
    dataIndex: 'username',
    copyable: true,
  },
  {
    title: '用户账户',
    dataIndex: 'userAccount',
    copyable: true,
  },
  {
    title: '头像',
    dataIndex: 'avatarUrl',
    render: (_, record) => (
      <div>
        <Image src={record.avatarUrl} width={100} />
      </div>
    ),
  },
  {
    title: '性别',
    dataIndex: 'gender',
  },
  {
    title: '电话',
    dataIndex: 'phone',
    copyable: true,
  },
  {
    title: '邮件',
    dataIndex: 'email',
    copyable: true,
  },
  {
    title: '状态',
    dataIndex: 'userStatus',
  },
  {
    title: '星球编号',
    dataIndex: 'planetCode',
  },
  {
    title: '角色',
    dataIndex: 'userRole',
    valueType: 'select',
    valueEnum: {
      0: { text: '普通用户', status: 'Default' },
      1: {
        text: '管理员',
        status: 'Success',
      },
    },
  },
  {
    title: '创建时间',
    dataIndex: 'createTime',
    valueType: 'dateTime',
  },
  {
    title: '操作',
    valueType: 'option',
    render: (text, record, _, action) => [
      <a
        key="editable"
        onClick={() => {
          action?.startEditable?.(record.id);
        }}
      >
        编辑
      </a>,
      <a href={record.url} target="_blank" rel="noopener noreferrer" key="view">
        查看
      </a>,
      <TableDropdown
        key="actionGroup"
        onSelect={() => action?.reload()}
        menus={[
          { key: 'copy', name: '复制' },
          { key: 'delete', name: '删除' },
        ]}
      />,
    ],
  },
];

export default () => {
  const actionRef = useRef<ActionType>();
  return (
    <ProTable<API.CurrentUser>
      columns={columns}
      actionRef={actionRef}
      cardBordered
      request={async (params = {}, sort, filter) => {
        console.log(sort, filter);
        const userList = await searchUsers();
        return {
          data: userList,
        };
      }}
      editable={{
        type: 'multiple',
      }}
      columnsState={{
        persistenceKey: 'pro-table-singe-demos',
        persistenceType: 'localStorage',
      }}
      rowKey="id"
      search={{
        labelWidth: 'auto',
      }}
      form={{
        // 由于配置了 transform,提交的参与与定义的不同这里需要转化一下
        syncToUrl: (values, type) => {
          if (type === 'get') {
            return {
              ...values,
              created_at: [values.startTime, values.endTime],
            };
          }
          return values;
        },
      }}
      pagination={{
        pageSize: 5,
      }}
      dateFormatter="string"
      headerTitle="高级表格"
    />
  );
};

注销登录前端

1. 首先找到退出登录函数

2. 改一下Api

2.1. 点击退出登录发现成功跳转到登录页面。

在前端注册页面补充一个编号输入框

  • 补充一个输入框:

  • 写上对应的数据类型

  • 补充用户类型的定义:

  • 看一下效果:

注册一下,看一下数据库有数据,成功。

前端优化

1. 在前端定义通用返回对象,和后端的Response对应起来:
/**
 * 统一返回类型
 */
type BaseResponse<T> = {
  code: number;
  data: T;
  message: string;
  description: string;
};

2. 给register的响应类型封装⼀下

3. 将所有请求的参数进行封装

4. 由于我们返回的对象,数据是在对象的data里的,需要定义一个全局响应拦截器,把所有接⼝中响应的data全都取出来
request.interceptors.response.use(async (response, options): Promise<any> => {
  const res = await response.clone().json();
  if (res.code === 0) {
    return res.data;
  }
 }

如果code为0表示成功,就直接取出响应的data,这样的话前端就不需要做修改了。

request.ts属于.umi,它是这个框架⾃动帮我们⽣成的⽂件

5. 写一个请求类,覆盖默认的request方法

在Ant Design Pro中Umi-request全局请求响应的处理_umi request 允许修改响应数据-CSDN博客

/**
 * request 网络请求工具
 * 更详细的 api 文档: https://github.com/umijs/umi-request
 */

import { extend } from 'umi-request';
import { message } from 'antd';
import { history } from '@@/core/history';
import { stringify } from 'querystring';

/**
 * 配置request请求时的默认参数
 */
const request = extend({
  credentials: 'include', // 默认请求是否带上cookie
  prefix: process.env.NODE_ENV === 'production' ? 'http://user-backend.code-nav.cn' : undefined,
  // requestType: 'form',
});

/**
 * 所以请求拦截器
 */
request.interceptors.request.use((url, options): any => {
  console.log(`do request url = ${url}`);
  return {
    url,
    options: {
      ...options,
      headers: {},
    },
  };
});

/**
 * 所有响应拦截器
 */
request.interceptors.response.use(async (response, options): Promise<any> => {
  const res = await response.clone().json();
  if (res.code === 0) {
    return res.data;
  }
  if (res.code === 40100) {
    message.error('请先登录');
    history.replace({
      pathname: '/user/login',
      search: stringify({
        redirect: location.pathname,
      }),
    });
  } else {
    message.error(res.description);
  }
  return res.data;
});

export default request;
6. 在api.ts引⼊我们⾃⼰写的

测试一下:已经正常地抛出错误了

非管理员:

管理员:

  • 28
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柳智麒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值