1项目开发流程及技术选型 流程 需求分析 => 设计(概要设计、详细设计) => 技术选型 => 初始化 / 引入需要的技术 => 写Demo => 写代码(实现业务逻辑) => 测试(单元测试) => 代码提交 / 代码评审 => 部署 => 发布
项目流程解释
需求分析 ◦需求收集整理:将项目需要的信息进行分类、排序 ◦需求分析验证:对需求深入分析,包括业务逻辑、约束条件和潜在风险,确认需求的准确性和完整性,确保需求都是清晰、可衡量和可实现的 ◦需求文档化:项目执行和后期参考的依据
概要设计、详细设计 概要设计:确定总体布局 ▪系统总体架构:定义整体结构(硬件、软件、网络等) ▪功能模块划分:划分模块,明确各模块的功能和职责 ▪接口设计:定义模块之间接口,确保模块间数据交互流畅 ▪技术选型:根据需求以及技术的发展,选择适合的技术栈和工具链 ▪数据库逻辑设计:数据库的逻辑结构(表结构、索引、引脚等) ▪用户界面设计:初步设计用户界面的布局和交互方式
详细设计:细化各个模块的设计 ▪算法设计:功能模块设计具体算法,实现业务逻辑 ▪数据结构设计:设计详细的数据结构(数据库) ▪接口物理定义:明确模块间接口的物理实现方式(Eg:API接口定义) ▪用户界面细化:详细设计用户界面的布局、样式、交互逻辑等 ▪错误处理与安全性设计:考虑系统可能出现的异常情况,设计相应的错误处理机制和安全性措施 3. 技术选型 根据需求及技术的发展选择适合的技术栈和工具链 4. 初始化 / 引入需要的技术 ◦根据技术选型的结果,搭建开发环境,引入所需的框架、库等,并进行必要的配置 ◦新项目:初始化 ◦已有项目:引入需要的技术 5. 写Demo 在正式开始编码之前,编写一个简单的演示程序(Demo),验证技术选型的可行性和设计的合理性(Eg:测试组件库是否引入成功) 6. 写代码(实现业务逻辑) 根据详细设计文档,编写实现业务逻辑的代码。需要遵循编码规范,保证代码可读性、可维护性和可扩展性 7. 测试(单元测试) 编写单元测试代码,对项目的各个模块进行测试,确保它们按照预期工作 8. 代码提交 / 代码评审 将编写好的代码提交到代码仓库,并进行代码评审。代码评审可以帮助发现潜在的问题,提高代码质量。 9. 部署 将代码部署到生产环境或测试环境,进行集成测试和性能测试 10. 发布 经过充分的测试后,将项目发布,进行实际使用以及后续进行的维护和升级
需求分析与技术选型
登录 / 注册
用户管理(仅管理员可见)对用户的查询或者修改
用户校验(仅星球用户)
前端:三件套 + React + 组件库 Ant Design + Umi + Ant Design Pro(现成的管理系统) 后端:Java + MySQL •Spring (依赖注入框架,帮助你管理 Java 对象,集成一些其他的内容) •SpringMVC (web 框架,提供接口访问、restful接口等能力) •Mybatis (Java 操作数据库的框架,持久层框架,对 jdbc 的封装) •Mybatis-plus (对 mybatis 的增强,不用写 sql 也能实现增删改查) •SpringBoot (快速启动 / 快速集成项目。不用自己管理 spring 配置,不用自己整合各种框架) •Junit (单元测试库) 部署:服务器 / 容器(平台)
2前端初始化 Ant Design 组件引入 Ant Design Pro 官网 GitHub 项目链接
官网点击开始使用 ,找到初始化依次执行
操作步骤【跟鱼皮保持一致版本3.1.0】
管理员方式打开命令行 执行下面命令npm i @ant-design/pro-cli@3.1.0 -gnpm i @ant-design/pro-cli -g
查看ant-design-pro的版本号,保证是3.1.0pro -v
下载镜像(进度条不动处理)npm config set registry https://registry.npmmirror.com
查看是否成功npm config get registry
进入要创建项目的文件目录下,打开命令行输入如下命令
1 2 3 4 pro create myapp #如果安装了 nvm 使用如下命令 npx pro create myapp
全称为 node.js version management,顾名思义是用于管理多个 nodejs 的版本控制工具 。 通过 nvm 可以安装和切换不同版本的 nodejs。使用教程 选择 umi@3 、 simple
使用 WebStorm 打开项目,安装依赖
或者采用鱼皮视频中输入命令 yarn 或 npm 的方法 依赖安装完成后,在 package.json 文件中点击 start => Run start 运行项目 项目启动成功后,点击 http://localhost:8000 访问"start": "cross-env UMI_ENV=dev umi dev","start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev umi dev", 两个启动命令的区别:start-dev 中的 MOCK = none 是关闭模拟数据,此时没有后台数据,登录使用的数据是模拟数据,故使用 start-dev 启动项目时会登陆失败
配置umi UI,点击Teriminal,输入如下命令
输入yarn add @umijs/preset-ui -D会报错提示node版本不兼容npm install --save-dev @umijs/preset-ui 再start启动项目,可以看到右下角出现这个工具(目前没显示,已忽略)
框架瘦身 每次删除一个文件,都重启项目看是否能运行
在 package.json 中找到 i18n-remove (移除国际化的脚本)执行
目录结构
├── config # umi 配置,包含路由,构建等配置【下面的oneapi.json(定义整个项目用到的接口)删】 ├── mock # 本地模拟数据 ├── public # 一些静态资源(logo、图标等) │ └── favicon.png # Favicon ├── src │ ├── assets # 本地静态资源 │ ├── components # 业务通用组件 │ ├── e2e # 集成测试用例【删】 │ ├── layouts # 通用布局 │ ├── models # 全局 dva model │ ├── pages # 业务页面入口和常用模板 │ ├── services # 后台接口服务【下面的swagger(接口文档工具)删】 │ ├── utils # 工具库 │ ├── locales # 国际化资源【删】 │ ├── global.less # 全局样式 │ └── global.ts # 全局 JS ├── tests # 测试工具【删】 ├── README.md └── package.json jest.config.ts # 测试工具 【删】 playwright.config.ts # 自动化测试工具,帮助在火狐、谷歌自动测试,不用真实打开浏览器 【删】
其他文件介绍 app.tsx 项目全局入口文件,定义了整个项目中使用的公共数据(比如用户信息) global.less 全局引用样式(尽量不动),可理解为 css global.tsx 全局脚本文件,可理解为 js service-worker.js 一些前端页面的缓存 typings.d.ts 定义了一些 ts 的类型,类似C语言中的宏定义 (#) .editorconfig 编辑器的配置 .eslintrc.js 检查 js 语法是否规范 prettierrc.js 美化前端代码的工具 jest.config.js 测试工具【删】 .stylelintrc.js 检查 css 语法是否规范 playwright.config.ts 自动化测试工具,帮助在火狐、谷歌自动测试,不用真实打开浏览器 【删】 asscess.ts 控制用户的访问权限【链接】
•【删除第三个config下的oneapi.json文件时 删了时候无法运行解决:】 •【app.tsx文件Parameter *** implicity has *** type爆红解决:】
修改标题和Logo
3后端初始化 创建项目 安装 MySQL 数据库 3种方式初始化 Java 项目
Github 上搜索 SpringBoot 模板,拉取(不推荐)
SpringBoot 官方模板生成器 )
IDEA 生成(推荐)
环境搭建
创建项目 spring.io 不支持勾选 Java 8 了 , 可以如下图所示,点击齿轮,将初始化链接换成阿里云的http://start.aliyun.com , 就可以正常选择 Java 8
连接数据库 root 123456
Spring Boot 框架整合 (一)创建数据库 (二)按照 mybatis-plus 官方教程 操作
建表(官方教程复制)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #建表 DROP TABLE IF EXISTS user ;CREATE TABLE user ( id BIGINT NOT NULL COMMENT '主键ID' , name VARCHAR (30 ) NULL DEFAULT NULL COMMENT '姓名' , age INT NULL DEFAULT NULL COMMENT '年龄' , email VARCHAR (50 ) NULL DEFAULT NULL COMMENT '邮箱' , PRIMARY KEY (id) ); #插入假数据 DELETE FROM user ;INSERT INTO user (id, name, age, email) VALUES (1 , 'Jone' , 18 , 'test1@baomidou.com' ), (2 , 'Jack' , 20 , 'test2@baomidou.com' ), (3 , 'Tom' , 28 , 'test3@baomidou.com' ), (4 , 'Sandy' , 21 , 'test4@baomidou.com' ), (5 , 'Billie' , 24 , 'test5@baomidou.com' );
pom.xml 文件中引入依赖 junit 从 maven 仓库 中引入
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.2</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.13.2</version > <scope > test</scope > </dependency >
配置 application.properties 改成 application.yml面试鸭 - Spring Boot 中 application.properties 和 application.yml 的区别是什么
视频中需要更新的地方:driver-class-name: com.mysql.cj.jdbc.Driver
1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: application: name: user-center datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/yupi username: 你的用户名 password: 你的密码 server: port: 8080
在 Spring Boot 启动类中添加 @MapperScan 注解,扫描 Mapper 文件夹(没有mapper,新建一个)
1 2 3 4 5 6 7 8 @SpringBootApplication @MapperScan("com.yupi.usercenter.mapper") public class UserCenterApplication { public static void main (String[] args) { SpringApplication.run(UserCenterApplication.class, args); } }
编码 编写实体类 User.java:
1 2 3 4 5 6 7 @Data public class User { private Long id; private String name; private Integer age; private String email; }
编写 Mapper 接口类 UserMapper.java:
1 2 3 public interface UserMapper extends BaseMapper <User> {}
开始使用 添加测试类,进行功能测试(两种方式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @SpringBootTest class UserCenterApplicationTests {@Resource private UserMapper userMapper;@Test void contextLoads () { System.out.println(("----- selectAll method test ------" )); List userList = userMapper.selectList(null ); Assert.isTrue(5 == userList.size(), "" ); userList.forEach(System.out::println); } }
1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootTest public class SampleTest {@Resource private UserMapper userMapper;@Test public void testSelect () { System.out.println(("----- selectAll method test ------" )); List userList = userMapper.selectList(null ); Assert.isTrue(5 == userList.size(), "" ); userList.forEach(System.out::println); } }
数据库表设计
性别是否需要索引? 不需要,区分度不高的字段。只有2种取值的字段,建了索引数据库也不一定会用,只会白白增加索引维护的额外开销,因为索引也是需要存储的,所以插入和更新的写入操作,同时需要插入和更新你这个字段的索引的。 所以说,唯一性太差的字段不需要创建索引,即便用于where条件
删除测试 Mybatis-plus 是否引入成功时创建的 user 表,执行如下语句
1 DROP TABLE IF EXISTS `user `
建表,执行如下建表语句(或者使用 idea、可视化工具建表)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 DROP TABLE IF EXISTS 'user' CREATE TABLE 'user' ( `username` varchar (256 ) NULL COMMENT '用户昵称' , `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id' , `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' NULL COMMENT '状态 0-正常' , `createTime` datetime DEFAULT CURRENT_TIMESTAMP NULL COMMENT '创建时间' , `updateTime` datetime DEFAULT CURRENT_TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' , `isDelete` tinyint DEFAULT '0' NOT NULL COMMENT '是否删除' , PRIMARY KEY (`id`) ) COMMENT= '用户表' ;
规整项目目录 新建 controller、service、utils 包 删除测试 Mybatis-plus 是否引入成功时创建的 User、UserMapper、SampleTest【图解 Spring Boot 五层结构】
文件
说明
补充说明
controller
请求层/控制层 (只接收请求)
应用程序的入口点,负责接收用户的请求并返回响应。 调用Service层的方法来处理业务逻辑,并将结果返回给前端用户
service
业务逻辑层 (专门编写业务逻辑,例如登录注册)
程序核心,负责处理业务逻辑。 接收Controller层的请求,调用Mapper层执行数据库操作,并将处理结果返回给Controller层。 还可以包含一些复杂的业务逻辑处理,如事务管理、数据校验等。
mapper / dao
数据库访问层 (专门从数据库中对数据进行增删改查操作)
负责与数据库交互,执行数据的增删改查(CRUD)操作。 通常包含了一系列的接口,定义了数据库操作的方法,而具体的SQL实现则位于Mapper XML文件中
model / entity / pojo
实体层 (定义了一些和数据库相关的数据模型、封装类) (可能需要分层 entity、dto、vo……)
定义数据模型,即数据库表的映射实体类。 实体类中包含了与数据库表相对应的属性和方法(如get和set方法,toString 方法,有参无参构造函数)
utils
存放一些工具类 (加密、格式转换、日期转换…… 与业务关系不太大的类)
static
写前后端不分离的项目时,放一些静态文件(html)
4后端注册登录功能 后端-代码生成器Mybatis-X的使用 Mybatis-X插件,自动根据数据库生成 •domain(实体对象) •mapper(操作数据库的对象) •mapper.xml(定义了mapper对象和数据库的关联,可以在里面自己写SQL) •service(包含常用的增删改查) ◦serviceImpl(具体实现service)
插件的使用 右键数据库表,点击第一行路径默认
勾选 Comment 可以在生成的实体类添加注释 勾选 Actual Column 可以让生成实体类的字段名和数据库保持一致
◦domain 放入 model 包 ◦UserMapper 放入mapper 包 ◦impl 和 UserService 放入 service 包 ◦删除 generator
测试 ◦鼠标放在 UserService.java 文件的 UserService 上,按住 alt + enter ,选择创建测试类 ◦编写测试代码【安装插件 CenerateAllSetter ,可快速生成代码】 完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @SpringBootTest class UserServiceTest {@Resource private UserService userService;@Test void testAddUser () { User user = new User (); user.setUsername("dogYupi" ); user.setUserAccount("123" ); user.setAvatarUrl("https://profile-avatar.csdnimg.cn/aee7bf7d9b0640489ce4189664a41212_2201_75344078.jpg!1" ); user.setGender(0 ); user.setUserPassword("xxx" ); user.setPhone("123" ); user.setEmail("456" ); boolean result = userService.save(user); System.out.println(user.getId()); Assertions.assertTrue(result); }
执行测试,会发现报错
问题解决 ◦问题原因:MyBatisX自动开启从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射 ◦问题解决:yml 文件中添加如下配置官网描述 【链接已更新】
1 2 3 mybatis-plus: configuration: map-underscore-to-camel-case: false
再次测试
详细设计 注册接口
用户在前端输入账户和密码、以及校验码(todo)
校验用户的账户、密码、校验密码是否符合要求 ◦账户不小于 4 位(自己扩展校验) ◦密码不小于 8 位 ◦账户不能重复 ◦账户不包含特殊字符 ◦密码和校验密码相同
对密码进行加密(千万不能明文存储到数据库中)
向数据库插入用户数据
登录接口
接收参数:用户账户、密码
请求类型:POST 请求参数很长时不建议用 GET
请求体:JSON格式的数据
返回值:用户信息(脱敏)
逻辑
校验用户账户和密码是否合法 ◦非空 ◦账户不小于 4 位 ◦密码不小于 8 位 ◦账户不包含特殊字符
校验密码是否输入正确,要和数据库中的密文密码对比
用户脱敏,隐藏敏感信息,防止数据库中的字段泄露
我们要记录用户的登录态(session),将其存到服务器上(用后端SpringBoot框架封装的服务器tomcat去记录) ◦cookie
返回安全的脱敏后的用户信息
后端-接口测试开发
Tips: 如何快速生成注释 在方法上打出 /** 回车即可
src/main/java/fun.weiguang/usercenter/service/UserService.java 编写 userRegister 方法并实现
去 maven 仓库 引入 commons-lang3 依赖
1 2 3 4 5 6 <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-lang3</artifactId > <version > 3.12.0</version > </dependency >
UserServiceImpl 实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Service public class UserServiceImpl extends ServiceImpl <UserMapper, User> implements UserService { @Resource private UserMapper userMapper; @Override 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() < 8 || checkPassword.length() < 8 ) { return -1 ; } String validateRegExp = "\\pP|\\pS|\\s+" ; Matcher matcher = Pattern.compile(validateRegExp).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 = "yupi" ; 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(); } }
src/test/java/com/yupi/usercenter/UserCenterApplicationTests.java 中编写测试,执行(加密成功)
后端-单元测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ServiceTest 测试代码生成与编写 @Test void userRegister () { String userAccount = "yupi" ; String userPassword = "" ; String checkPassword = "123456" ; long result = userService.userRegister(userAccount, userPassword, checkPassword); Assertions.assertEquals(-1 , result); userAccount = "yu" ; result = userService.userRegister(userAccount, userPassword, checkPassword); Assertions.assertEquals(-1 , result); userAccount = "yupi" ; userPassword = "123456" ; result = userService.userRegister(userAccount, userPassword, checkPassword); Assertions.assertEquals(-1 , result); userAccount = "yu pi" ; userPassword = "12345678" ; result = userService.userRegister(userAccount, userPassword, checkPassword); Assertions.assertEquals(-1 , result); checkPassword = "123456789" ; result = userService.userRegister(userAccount, userPassword, checkPassword); Assertions.assertEquals(-1 , result); userAccount = "dogYupi" ; checkPassword = "12345678" ; result = userService.userRegister(userAccount, userPassword, checkPassword); Assertions.assertEquals(-1 , result); userAccount = "yupi" ; result = userService.userRegister(userAccount, userPassword, checkPassword); Assertions.assertTrue(result > 0 ); }
执行 报错了【电脑在 idea 更新了一些配置后这里不会报错了,依旧报错按照下面更换正则表达式】 •Java 过滤特殊字符的正则表达式换成 [`~!@#$%& ()+=|{}’:;’,[].<>/?~!@#¥%……& ()——+|{}【】‘;:”“’。,、?] 或者 [ a-zA-Z0-9] (如果不行,搜索合适的正则表达式[A-Za-z0-9_-\u4e00-\u9fa5]+), 再次执行,报错了(是因为数据库中没有dogYupi 这个账户,设置与其返回值为 -1,所以报错)
登录功能编写代码
UserService 编写如下代码,Alt + Enter 实现方法
进入 Impl 将 Register 的逻辑复制过来,进行修改 ◦将 final String SALT = “yupi”; 提到前面 ◦添加 @Slf4j 注解,使用 log 【方便后续系统出现问题,到日志中查找问题】 ◦修改部分逻辑 【删除插入数据;账户不能重复修改成查询用户是否存在,放在加密之后】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 @Service @Slf4j public class UserServiceImpl extends ServiceImpl <UserMapper, User> implements UserService { @Resource private UserMapper userMapper; private static final String SALT = "yupi" ; @Override 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() < 8 || checkPassword.length() < 8 ) { return -1 ; } String validateRegExp = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+" ; Matcher matcher = Pattern.compile(validateRegExp).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 = "yupi" ; 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(); } @Override public User doLogin (String userAccount, String userPassword) { if (StringUtils.isAnyBlank(userAccount,userPassword)) { return null ; } if (userAccount.length() < 4 ){ return null ; } if (userPassword.length() < 8 ){ 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("user login failed,userAccount cannot match userPassword" ); return null ; } return user; }
上面查询用户是否存在的代码逻辑是存在问题的 如果用户的 isDelete 字段是删除状态,能否查出来呢? 【MyBatis-Plus 逻辑删除 @TableLogic注解 】
@TableLogic 用于标记实体类中的逻辑删除字段。 逻辑删除是一种数据管理策略,它不是真正地从数据库中删除记录,而是在记录中标记该记录为已删除状态。 通过使用@TableLogic注解,MyBatis-Plus 可以在查询、更新和删除操作中自动处理逻辑删除字段的值。
当执行查询操作时,MyBatis-Plus 会自动过滤掉标记为逻辑删除的记录,只返回未删除的记录。 在执行更新操作时,如果更新操作会导致逻辑删除字段的值变为逻辑删除值,MyBatis-Plus 会自动将该记录标记为已删除。 在执行删除操作时,MyBatis-Plus 会自动将逻辑删除字段的值更新为逻辑删除值,而不是物理删除记录。 使用@TableLogic注解可以实现数据的逻辑删除,有助于维护数据的完整性和可追溯性,同时避免了物理删除操作可能带来的数据丢失风险。 开发者无需手动编写逻辑删除的代码,MyBatis-Plus 会自动处理这一过程。
要删除一条数据时,不是真正的删除,而是将数据库中的某个字段从 0 置为 1 表示数据失效,无法查询到
在 application.yml 中配置 MyBatis-Plus 的全局逻辑删除属性:
1 2 3 4 5 6 mybatis-plus: global-config: db-config: logic-delete-field: isDelete logic-delete-value: 1 logic-not-delete-value: 0
User 实体类中 为 isDelete 字段添加 @TableLogic 注解
该注解用于标记实体类中的字段作为逻辑删除字段。 逻辑删除是一种数据管理策略,它不是真正地从数据库中删除记录,而是在记录中标记该记录为已删除状态。 通过使用@TableLogic注解,MyBatis-Plus 可以在查询、更新和删除操作中自动处理逻辑删除字段的值。
后端-登录态管理(Cookie 和 Session) 如何知道是哪个用户登陆了?(JavaWeb)
连接服务器端后,得到一个 session 状态(匿名会话),返回给前端
登陆成功得到登陆成功的 session 并设置一些值(比如用户信息),返回给前端一个设置 cookie 的“命令” session ⇒ cookie
前端接收到后端的命令后,设置 cookie ,保存到浏览器内
前端再次请求后端的时候(相同的域名),在请求头中带上 cookie去请求
后端拿到前端传来的 cookie ,找到对应的 session
后端从 session 中可以取出基于该 session 存储的变量(用户的登陆信息、登录名) 【Cookie & Session 相关知识了解 】
Cookie
Session
定义
Cookie 是一种存储在用户本地计算机上的小数据片段。它由服务器发送,浏览器保存,并在后续请求中自动发送回服务器。Cookie 主要用于识别用户身份、存储用户偏好等
Session 是一种服务器端的技术,用于存储用户会话信息。服务器会为每个用户创建一个唯一的 Session ID,并通过 Cookie(或其他方式,如 URL 重写)将 Session ID 发送给客户端。客户端在后续请求中会携带这个 Session ID,服务器通过解析 Session ID 来获取对应的 Session 信息
特点
•存储在客户端 •容量有限(大多数浏览器限制每个 Cookie 的大小不超过 4KB,且每个站点最多可以创建 20 个 Cookie) •安全性较低,因为 Cookie 存储在用户的计算机上,可以被用户看到并修改(尽管可以通过设置 HttpOnly 和 Secure 属性来提高安全性) •发送 Cookie 会增加每次 HTTP 请求的数据量,可能影响页面加载速度
•存储在服务器端 •安全性较高,因为敏感信息不会直接存储在客户端。 •容量相对较大,因为受限于服务器的资源而非客户端的浏览器限制。 •依赖于客户端的 Session ID 传递
用途
•用户身份识别。 •存储用户偏好设置。 •跟踪用户行为(例如,记录用户的访问次数)
•存储用户会话信息,如登录状态、购物车内容等。 •管理用户登录状态,实现身份验证和授权
联系
•Session 通常依赖于 Cookie 来传递 Session ID,以维持用户会话的连续性。 •两者都可用于跟踪用户状态,但存储位置和数据量等方面有所不同
区别
•存储位置:Cookie 存储在客户端,Session 存储在服务器端。 •安全性:Session 相比 Cookie 安全性更高,因为敏感数据不直接存储在客户端。 •容量:Cookie 的容量有限,Session 的容量相对较大。 •使用场景:Cookie 更适合存储非敏感信息,如用户偏好;Session 更适合存储敏感信息,如用户登录状态
代码编写
UserService 与 UserServiceImpl 中添加 HttpServletRequest request
继续在 Impl 中编写代码 ◦记录用户的登录态
在前面定义用户登录态键
编写记录用户登录态代码,将 user 修改为 safetyUser ◦用户脱敏
编写生成用户脱敏代码 修改生成的代码
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @Override public User doLogin (String userAccount, String userPassword, HttpServletRequest request) { if (StringUtils.isAnyBlank(userAccount,userPassword)) { return null ; } if (userAccount.length() < 4 ){ return null ; } if (userPassword.length() < 8 ){ return null ; } String validPattern = "[`~!@#$%^&()+=|{}':;',\\[\\].<>/?~!@#¥%……&()——+|{}【】‘;:”“’。,、?]" ; Matcher matcher = Pattern.compile(validPattern).matcher(userAccount); if (matcher.find()) { return null ; } String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes()); QueryWrapper queryWrapper = new QueryWrapper <>(); queryWrapper.eq("userAccount" ,userAccount); queryWrapper.eq("userPassword" ,encryptPassword); User user = userMapper.selectOne(queryWrapper); if (user == null ) { log.info("user login failed,userAccount cannot match userPassword" ); return null ; } User safetyUser = new User (); safetyUser.setId(user.getId()); safetyUser.setUsername(user.getUsername()); safetyUser.setUserAccount(user.getUserAccount()); safetyUser.setAvatarUrl(user.getAvatarUrl()); safetyUser.setGender(user.getGender()); safetyUser.setPhone(user.getPhone()); safetyUser.setEmail(user.getEmail()); safetyUser.setUserStatus(user.getUserStatus()); safetyUser.setCreateTime(user.getCreateTime()); request.getSession().setAttribute(USER_LOGIN_STATE,safetyUser); return safetyUser; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 package fun.weiguang.usercenter.service;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import fun.weiguang.usercenter.model.domain.User;import fun.weiguang.usercenter.mapper.UserMapper;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.springframework.util.DigestUtils;import org.springframework.stereotype.Service;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import java.util.regex.Matcher;import java.util.regex.Pattern;@Service @Slf4j public class UserServiceImpl extends ServiceImpl <UserMapper, User> implements UserService { @Resource private UserMapper userMapper; private static final String SALT = "yupi" ; private static final String USER_LOGIN_STATE = "userloginstate" ; @Override 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() < 8 || checkPassword.length() < 8 ) { return -1 ; } String validateRegExp = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+" ; Matcher matcher = Pattern.compile(validateRegExp).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 = "yupi" ; 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(); } @Override public User doLogin (String userAccount, String userPassword, HttpServletRequest request) { if (StringUtils.isAnyBlank(userAccount,userPassword)) { return null ; } if (userAccount.length() < 4 ){ return null ; } if (userPassword.length() < 8 ){ return null ; } String validPattern = "[`~!@#$%^&()+=|{}':;',\\[\\].<>/?~!@#¥%……&()——+|{}【】‘;:”“’。,、?]" ; Matcher matcher = Pattern.compile(validPattern).matcher(userAccount); if (matcher.find()) { return null ; } String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes()); QueryWrapper queryWrapper = new QueryWrapper <>(); queryWrapper.eq("userAccount" ,userAccount); queryWrapper.eq("userPassword" ,encryptPassword); User user = userMapper.selectOne(queryWrapper); if (user == null ) { log.info("user login failed,userAccount cannot match userPassword" ); return null ; } User safetyUser = new User (); safetyUser.setId(user.getId()); safetyUser.setUsername(user.getUsername()); safetyUser.setUserAccount(user.getUserAccount()); safetyUser.setAvatarUrl(user.getAvatarUrl()); safetyUser.setGender(user.getGender()); safetyUser.setPhone(user.getPhone()); safetyUser.setEmail(user.getEmail()); safetyUser.setUserStatus(user.getUserStatus()); safetyUser.setCreateTime(user.getCreateTime()); request.getSession().setAttribute(USER_LOGIN_STATE,safetyUser); return safetyUser; } }
后端-接口开发及测试 控制层Controller封装请求 application.yml 指定接口全局 api 【也可以可以等 代理 这块去做】
1 2 servlet: context-path: /api
@RestController适用于编写restful风格的api,返回值默认为json类型 controller 层倾向于对请求参数本身的校验,不涉及业务逻辑本身(越少越好) service 层是对业务逻辑的校验(有可能被 controller 之外的类调用)
Controller 包下新建 UserController.java , ◦添加 @RestController 注解(这个类中所有的请求的接口返回值,相应的数据类型都是 application json ◦添加 @RequestMapping 注解(定义请求的路径)
1 2 3 4 5 @RestController @RequestMapping("/user") public class UserController {}
下载插件 Auto filling Java call arguments 【自动填充 java 参数】(安装完成记得重启)
UserController 中编写 register请求 ◦编写注册请求
◦封装专门用来接收请求参数的对象 model.domain 包建立一个 request 包,新建 UserRegisterRequest.java 类
继承 Serializable (序列化),打上注解
实现序列化,添加参数,打上 @lombok 注解(生成get、set方法) ◦回到 UserController
引用 UserRegisterRequest ;打上 @RequestBody 注解(使UserRegisterRequest与前端传来的参数能够对应);判断是否为空
◦完善代码逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 @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); }
UserController 中编写 login 请求 ◦复制 register 请求,进行修改即可【重构 UserService 中的 doLogin 方法为 userLogin 方法】
1 2 3 4 5 6 7 8 9 10 11 12 @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); }
◦model.domain 包下面的 request 包,新建 UserLoginRequest.java 类(复制粘贴 UserRegisterRequest.java 删除其中的 checkPassword 即可)
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data public class UserLoginRequest implements Serializable {private static final long serialVersionUID = 你生成的序列号;private String userAccount;private String userPassword;}
测试 ◦测试工具(两种方式打开) ◦删除 GET 请求,生成 POST 请求并修改 ◦Debug 方式启动项目 ◦打断点测试 ◦测试完成 ◦再测一下逻辑删除 ▪数据库中把 yupi 的 isDelete 字段改为 1 ▪启动 POST 请求,测试完成
5用户管理功能 !!!必须鉴权 !!! 1.查询用户 ◦允许根据用户名查询 2.删除用户
后端开发 1.UserController 里编写 查询用户请求
1 2 3 4 5 6 7 8 @GetMapping("/search") public List<User> searchUsers (String username) { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); if (StringUtils.isNotBlank(username)) { queryWrapper.like("username" , username); } return userService.list(queryWrapper); }
UserController 里编写 删除用户请求
1 2 3 4 5 6 7 @PostMapping("/delete") public boolean deleteUser (@RequestBody long id) { if (id <= 0 ) { return false ; } return userService.removeById(id); }
问题:此时代码的接口是开放的,没有校验是否是管理员,任何人均可调用,不安全 ◦解决:增加一个用户角色的字段,进行身份校验 ▪增加用户角色字段
role 改为 userRole
role 为 userRole
使用 MyBatisX 插件重新生成代码,替换原来的 User.java 文件【替换后记得对 isDelete 字段添加 @TableLogic 注解】;mapper.xml 文件中记得引用 mapper 修改文件路径; 修改 UserServiceImpl 文件中用户脱敏的代码
修改 UserController 中的 getRole 为 getUserRole【下面完整代码是修改过的】
▪将 UserServiceImpl 中的用户登录态键 提到 UserService 里
回到 UserController 编写鉴权逻辑
此时需要将 1 定义为一个常量,所以新建一个常量(新建包 constant ,constant 下建立 UserConstant 接口),将 用户登陆态键 修改到这里
点击左边图片中的报错提示,修改两处错误
回到 UserController 修改鉴权逻辑的代码,继续优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @GetMapping("/search") public List<User> searchUsers (String username,HttpServletRequest request) { if (!isAdmin(request)){ return null ; } QueryWrapper<User> queryWrapper = new QueryWrapper <>(); if (StringUtils.isNotBlank(username)) { queryWrapper.like("username" , username); } return userService.list(queryWrapper); } @PostMapping("/delete") public boolean deleteUser (@RequestBody long id,HttpServletRequest request) { if (!isAdmin(request)){ return false ; } if (id <= 0 ) { return false ; } return userService.removeById(id); } private boolean isAdmin (HttpServletRequest request) { Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE); User user = (User) userObj; return user != null && user.getUserRole() == ADMIN_ROLE; }
修改 yml 配置文件,设置 session 失效时间
删除 target 文件,找到之前请求的历史,执行重置登录态,点击 F9 结束(或者取消断点)
点击下面图标,执行 GET 请求
执行成功,可以看到返回了数据库的数据,但返回的数据有点多
修改代码进行过滤 ① UserServiceImpl 新建 getSafetyUser 方法,将之前编写的 用户脱敏的代码剪切过去 ② 将 user 重构为 originUser ③ 原来 用户脱敏 的位置调用 getSafetyUser 方法 ④ 添加注解 @Override 将方法引入 UserService 接口,加上注释
UserController 调用 getSafetyUser 方法修改返回数据信息
简化代码【两种方法:点击黄色灯泡;return 处Alt+Enter】
◦优化后的代码查询代码
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/search") public List<User> searchUsers (String username,HttpServletRequest request) { if (!isAdmin(request)){ return null ; } 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()); }
⚠写代码流程 • 先做设计 • 代码实现 • 持续优化!!!(复用代码、提取公共逻辑/常量)
前端-页面开发及验证 ctrl + shift + - :全部折叠代码 Ctrl + Shift + R : 全局替换
start-dev 启动(不带模拟数据)之前初始化的项目
修改底部信息 ◦修改 src/components/Footer/index.tsx 的链接(修改自己想链接到的地址即可)
◦修改后代码及样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const Footer: React.FC = () => { const defaultMessage = '鱼皮出品' ; const currentYear = new Date ().getFullYear(); return ( <DefaultFooter copyright={`${currentYear} ${defaultMessage}`} links={[ { key: 'planet' , title: '知识星球' , href: 'https://docs.qq.com/doc/DUG93dVNHbVZjZXpo' , blankTarget: true , }, { key: 'codeNav' , title: '编程导航' , href: 'https://www.code-nav.cn/' , blankTarget: true , }, { key: 'github' , title: <><GithubOutlined /> 鱼皮 GitHub </>, href: 'https://github.com/liyupi' , blankTarget: true , }, ]} /> ); };
修改 Logo ◦编写全局常量,新建 constants 文件夹,新建 index.ts 文件,编写一下内容
1 2 3 4 5 6 export const SYSTEM_LOGO = 'https://pic.code-nav.cn/user_avatar/1610518142000300034/YeedIoq3-logo.png' ; export const PLANET_LINK = 'https://docs.qq.com/doc/DUG93dVNHbVZjZXpo'
◦修改 src/pages/user/Login/index.tsx 文件中的 logo 引用; title ;subTitle
◦在 src/components/Footer/index.tsx 中引用 PLANET_LINK
◦修改后样式如下
4.删代码精简页面(src/pages/user/Login/index.tsx 文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 return ( <div className={styles.container}> return ; } message.success('获取验证码成功!验证码为:1234' ); }} /> </> )} <div style={{ marginBottom: 24 , }} > <ProFormCheckbox noStyle name="autoLogin" > 自动登录 </ProFormCheckbox> <a style={{ float : 'right' , }} href={PLANET_LINK} target="_blank" rel="noreferrer" > 忘记密码请联系鱼皮 </a> </div> </LoginForm> </div> <Footer /> </div> );
◦将 用户名 全局替换为 账号【ctrl + r】 ◦精简后页面
查看登录的提交方式,修改【ctrl + r 重构】
进入这个参数
修改 username 与 password 与后端字段一致
修改 username
修改 password
进入 login 方法
进入 LoginResult 方法
前后端联调 •前端需要向后端发送请求 •前端 ajax 请求后端 •axios 封装了 ajax •request 是 ant design项目又封装了一次 ◦追踪 request 源码:用到了 umi 的插件、requestConfig是一个配置
进入 login 方法
进入 request 方法
进入 getRequestMethod 方法
进入 plugin
看到插件是 umi
Ant Design Pro 搜索请求
复制上面官网搜到的代码,粘贴到 app.tsx 文件中,修改一下
修改 api.ts 文件中的登录接口访问的地址
进行登录测试【没有启动项目的先启动项目,start-dev 方式启动】
开发人员工具:点击浏览器右上方三个点,有的会直接显示,有的需要点击更多工具,查看快捷键
账号密码随便输 ◦此时遇到了跨域问题 ▪前端端口:localhost:8000 ▪后端端口:localhost:8080 ▪此时前端访问后端,端口不一样,就是跨域 ▪解决:搭建代理、后端支持跨域【这种方法不安全】
代理知识 正向代理:替客户端向服务器发送请求 反向代理:替服务器接收请求 怎么搞代理? •Nginx 服务器 •Node.js 服务器
代码修改
前端 ◦app.tsx 文件中将超时时间调长(代码来源) TypeScript
◦proxy.ts 文件
TypeScript
◦api.ts 文件中添加 /api
后端 (前面指定过不需要再次指定) application.yml 文件中指定全局 api
1 2 3 4 server: port: 8080 servlet: context-path: /api
测试,UserController 中这个位置打一个断点,前端再次进行登录测试
打断点
前端登录测试,访问地址正确
后端接收到请求
原本请求:http://localhost:8000/api/user/login 代理到请求:http://localhost:8080/api/user/login
6前端登录注册功能 前端-请求库的使用 代码修改
•登陆界面的 index.tsx 中
◦username => userAccount 、password => userPassword ,与后端保持一致【ctrl + r】
1 2 3 4 5 6 7 8 <ProFormText name="userAccount" …… /> <ProFormText.Password name="userPassword" …… />
◦对密码增加校验 【Ant Design】
名称
说明
类型
min
必须设置 type:string 类型为字符串最小长度;number 类型时为最小值;array 类型时为数组最小长度
number
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <ProFormText.Password …… rules={[ { required: true , message: '密码是必填项!' , }, { min: 8 , type: "string" , message: '密码长度不能小于8位!' , } ]} />
◦修改登录判断逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const handleSubmit = async (values: API.LoginParams) => { try { const user = await login ({ ...values, type, }) ; if (user) { …… } console.log(msg); setUserLoginState(user); } catch (error) { const defaultLoginFailureMessage = '登录失败,请重试!' ; message.error(defaultLoginFailureMessage); } };
◦使用已有账号进行登陆测试,可以看到登陆成功,响应有信息
注册功能 前端-快速页面开发 MFSU:前端编译优化 代码修改 1.删除 Login/index/tsx 中无用的引用代码【ctrl + alt + o】(平时自己做项目的细节点)
2.复制 Login 粘贴为 Register
3.将下面两个位置的 Login => Register
1 2 3 4 const Register: React.FC = () => { …… }; export default Register;
4.config/routes.ts 文件中增加 register 的路由(即地址栏中输入什么会跳转到注册页面
1 2 3 4 5 routes: [ { name: '登录' , path: '/user/login' , component: './user/Login' }, { name: '注册' , path: '/user/register' , component: './user/Register' }, { component: './404' }, ],
此时访问 user/register 页面会自动重定向到 user/login 页面,无法进入 register 页面,原因:app.tsx 是项目的全局文件,里面有两个判断逻辑【Ant Design Pro 是一个后台管理系统,对每个页面进行校验,不登录不允许进入任何页面是合适的】
const fetchUserInfo = async () => { try { const msg = await queryCurrentUser(); return msg.data; } catch (error) { history.push(loginPath); } return undefined; };
// 如果没有登录,重定向到 login if (!initialState?.currentUser && location.pathname !== loginPath) { history.push(loginPath); }
5.修改 app.tsx 中的代码逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const fetchUserInfo = async () => { try { const msg = await queryCurrentUser () ; return msg.data; } catch (error) { } return undefined; }; …… const whiteList = ['/user/register' , loginPath];if (whiteList.includes(location.pathname)) { return ; } if (!initialState?.currentUser) { history.push(loginPath); }
可以访问 reigster 页面
6.修改前端样式(增加确认密码框、删除与 login 有关与 register 无关的一些逻辑) Ctrl + R 替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 const Register: React.FC = () => { const [userLoginState, setUserLoginState] = useState<API.LoginResult>({}); }, { min: 8 , type: 'string' , message: '密码长度不能小于8位!' , } ]} /> </> )} {status === 'error' && loginType === 'mobile' && <LoginMessage content="验证码错误" />} <div style={{ marginBottom: 24 , }} > <ProFormCheckbox noStyle name="autoLogin" > 自动登录 </ProFormCheckbox> <a style={{ float : 'right' , }} href={PLANET_LINK} target="_blank" rel="noreferrer" > 忘记密码请联系鱼皮 </a> </div> </LoginForm> </div> <Footer /> </div> ); }; export default Register;
7.修改后样式
但此时登录按钮没有变成注册,这是因为 loginForm 组件的原因
前端-表单组件使用 Tips: 快速定位文件位置 •Ant Design (组件库) => React •Ant Design ProComponents => Ant Design •Ant Design Pro (后台管理系统) => Ant Design、React、Ant Design ProComponents、其他的库
LoginForm【组件详情 】
代码修改 1.修改注册按钮 ◦进入 LoginParams 源码【CTRL+点击】,进入到 index.d.ts 文件 (组件的类型定义),快速定位文件位置,点击 index.js 即可看到组件源码
◦回到注册页面,添加配置,可以看到修改成功
1 2 3 4 5 6 7 8 9 <LoginForm submitter={{ searchConfig: { submitText: '注册' } }} …… </LoginForm>
2.提交逻辑 ◦进入 LoginParams
◦仿照 LoginParams 增加 RegisterParams
1 2 3 4 5 6 type RegisterParams = { userAccount?: string; userPassword?: string; checkPassword?: string; type?: string; };
◦将刚刚点击的 LoginParams 替换为 RegisterParams ,文件中全局替换 LoginParams
3.修改注册逻辑 ◦增加校验代码,进入 login
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const handleSubmit = async (values: API.RegisterParams) => { const {userAccount,userPassword,checkPassword} = values; if (userPassword !== checkPassword){ message.error('两次密码不一致' ); return ; } try { const user = await login (userAccount,userPassword,checkPassword) ; …… };
◦ login 方法中,仿照 login 方法增加 register 方法,返回注册页面修改 login 为 register
1 2 3 4 5 6 7 8 9 10 11 12 13 export async function register (body: API.RegisterParams, options?: { [key: string]: any }) { return request<API.LoginResult>('/api/user/register' , { method: 'POST' , headers: { 'Content-Type' : 'application/json' , }, data: body, ...(options || {}), }); }
◦进入上述 LoginResult 方法,仿照 LoginResult 增加 RegisterResult ,返回 api.ts 文件修改 LoginResult 为 RegisterResulttype RegisterResult = number;
4.回到注册页面,修改跳转页面代码逻辑,删除无用的引用【快捷键CTRL+Alt+o】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 const LoginMessage: React.FC<{ content: string; }> = ({ content }) => ( <Alert style={{ marginBottom: 24 , }} message={content} type="error" showIcon /> ); const Register: React.FC = () => { const [userLoginState, setUserLoginState] = useState<API.LoginResult>({}); const [type, setType] = useState<string>('account' ); const handleSubmit = async (values: API.RegisterParams) => { const {userPassword,checkPassword} = values; if (userPassword !== checkPassword){ message.error('两次密码输入不一致' ); return ; } try { const id = await register (values) ; if (id > 0 ) { const defaultLoginSuccessMessage = '注册成功!' ; message.success(defaultLoginSuccessMessage); if (!history) return ; const { query } = history.location; const { redirect } = query as { redirect: string; }; …… };
后端-API 接口测试 打断点测试 1.后端 UserController 中打断点
测试一下刚刚写的代码逻辑
2.前端将 app.tsx 中的时间调长,测试
1 2 3 export const request: RequestConfig = { timeout: 1000000 , };
注册失败,返回 -1【因为注册过,用户名重复,但没有提示,有待优化】 换一个用户名注册,再次测试,注册成功,跳转到登录页
但是此时跳转地址有些问题,用户没有 rediact 参数
3.修改注册页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 try { const id = await register (values) ; if (id > 0 ) { const defaultLoginSuccessMessage = '注册成功!' ; message.success(defaultLoginSuccessMessage); if (!history) return ; const { query } = history.location; const { redirect } = query as { redirect: string; }; history.push({ pathname: '/user/login' , query, }); return ; }else { throw new Error (`register error id = ${id}`); } } catch (error) { const defaultLoginFailureMessage = '注册失败,请重试!' ; message.error(defaultLoginFailureMessage); }
4.修改登录页面Space 组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <Space split={<Divider type="vertical" />}> <ProFormCheckbox noStyle name="autoLogin" > 自动登录 </ProFormCheckbox> <Link to="/user/register" >注册账户</Link> <a style={{ float : 'right' , }} href={PLANET_LINK} target="_blank" rel="noreferrer" > 忘记密码 </a> </Space>
优化解决【AI 给出方案】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 TypeScript <div style={{ display: 'flex' , marginBottom: 24 , justifyContent: 'space-between' , alignItems: 'center' , }} > <ProFormCheckbox noStyle name="autoLogin" > 自动登录 </ProFormCheckbox> <Link to="/user/register" > 注册账号 </Link> <a style={{ float : 'right' , }} href={PLANET_LINK} target="_blank" rel="noreferrer" > 忘记密码 </a> </div>
登录功能 前端-登录态管理 1.后端 ◦UserController 中添加获取当前登录态、信息接口
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/current") public User getCurrentUser (HttpServletRequest request) { Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE); User currentUSer = (User) userObj; if (currentUSer == null ){ return null ; } long userId = currentUSer.getId(); User user = userService.getById(userId); return userService.getSafetyUser(user); }
此时还没有判断是否被封号,存在问题,后续再优化
◦UserServiceImpl 中的 getSafetyUser 里面加一个判断
1 2 3 4 5 6 7 @Override public User getSafetyUser (User originUser) { if (originUser == null ) { return null ; } …… }
前端 ◦app.tsx 中 点击 queryCurrentUser 进入 api.ts ,修改代码,点击 CurrentUser 查看其返回的数据,结合数据库字段设计修改代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 app.tsx …… const fetchUserInfo = async () => { try { const msg = await queryCurrentUser () ; return msg.data; } catch (error) { } return undefined; }; …… api.ts export async function currentUser (options?: { [key: string]: any }) { return request<API.CurrentUser>('/api/user/current' , { method: 'GET' , ...(options || {}), }); } typings.d.ts declare namespace API { type CurrentUser = { id: number; username: string; userAccount: string; avatarUrl?: string; gender: number; phone: string; email: string; userStatus: number; userRole: number; createTime: Date; };
◦将 app.tsx 中之前定义的白名单的代码提到前面,修改变量名,修改引用的变量名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const isDev = process.env.NODE_ENV === 'development' ;const loginPath = '/user/login' ;const NO_NEED_LOGIN_WHITE_LIST = ['/user/register' , loginPath];…… export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => { …… if (NO_NEED_LOGIN_WHITE_LIST.includes(location.pathname)) { return ; } ……
◦修改 app.tsx 中下面代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 …… const fetchUserInfo = async () => { try { return await queryCurrentUser () ; } catch (error) { history.push(loginPath); } return undefined; }; if (NO_NEED_LOGIN_WHITE_LIST.includes(history.location.pathname)) { const currentUser = await fetchUserInfo () ; return { fetchUserInfo, currentUser, settings: defaultSettings, }; } const currentUser = await fetchUserInfo () ; return { fetchUserInfo, currentUser, settings: defaultSettings, }; export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => { return { rightContentRender: () => <RightContent />, disableContentMargin: false , waterMarkProps: { content: initialState?.currentUser?.username, },
◦登录测试,发现可以进入欢迎页,但用户名和头像一直加载、水印不显示
▪水印,到数据库添加数据,添加后上传,刷新页面,水印显示
▪头像,前端找到文件src/components/RightContent/AvatarDropdown.tsx 修改代码,刷新网页
1 2 3 4 5 6 7 8 9 10 11 12 13 …… if (!currentUser || !currentUser.username) { return loading; } …… return ( <HeaderDropdown overlay={menuHeaderDropdown}> <span className={`${styles.action} ${styles.account}`}> <Avatar size="small" className={styles.avatar} src={currentUser.avatarUrl} alt="avatarUrl" /> <span className={`${styles.name} anticon`}>{currentUser.username}</span> </span> </HeaderDropdown> );
7用户管理功能 •Ant Design Pro 框架为我们生成了一个表格页面,页面的源码在 src/pages/TableList/index.tsx 由于源码复杂,所以我们自己编写
前端开发(一) 1.文件夹下 src/pages 新建 Admin 文件夹,复制同级目录 user 文件夹下的 Register 文件粘贴到 Admin 文件夹,更名为 UserManage
2.到 config/routes.ts 文件中添加路由
3.访问一下,发现无权访问【routes.ts 文件中有一个 access: ‘canAdmin’ 限制只有管理员可访问】
路由组件是如何知道该账号是不是管理员呢?从某个地方全局获取 •首次访问页面(刷新页面),进入app.tsx ,执行 getInitialState 方法,其返回值会作为全局共享的数据 •access.tx 文件中判断用户的访问权限,canAdmin 的值为 true 时才允许访问(可以将 canAdmin 的值直接改为 true 试一试能不能访问) •同理,我们可以在 access.tx 文件中定义 vip 可以访问页面
4.修改 access.ts 文件中的判断
1 2 3 4 5 6 7 8 9 export default function access (initialState: { currentUser?: API.CurrentUser } | undefined) { const { currentUser } = initialState ?? {}; return { canAdmin: currentUser && currentUser.userRole === 1 , }; }
5.后台数据库中修改 dogYupi 为管理员,方便编写页面
6.删除 config/routes.ts 文件中二级管理页的路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export default [ …… { path: '/welcome' , name: '欢迎' , icon: 'smile' , component: './Welcome' }, { path: '/admin' , name: '管理页' , icon: 'crown' , access: 'canAdmin' , component: './Admin' , routes: [ { path: '/admin/user-manage' , name: '用户管理' , icon: 'smile' , component: './Admin/UserManage' }, { path: '/admin/sub-page' , name: '二级管理页' , icon: 'smile' , component: './Welcome' }, { component: './404' }, ], }, { name: '查询表格' , icon: 'table' , path: '/list' , component: './TableList' }, { path: '/' , redirect: '/welcome' }, { component: './404' }, ];
◦此时前端页面显示如下,跟应该显示的注册页面不一致
7.修改 src/pages/Admin.tsx 文件
1 2 3 4 5 6 7 8 9 10 11 12 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;
前端开发(二) 使用 ProComponents 高级表单 1.通过 columns 定义表格有哪些列 2.columns 属性 ◦dataIndex 对应返回数据对象的属性 ◦title 表格列名 ◦copyable 是否允许复制 ◦ellipsis 是否允许缩略 ◦valueType 用于声明这一列的类型(dataTime、select)
1.初始化 src/pages/Admin/UserManage/index.tsx 页面【删完就行】
1 2 3 4 5 6 7 8 9 10 import React from 'react' ;const Register: React.FC = () => { return ( <div id="userManage" > </div> ); }; export default Register;
2.到 ProComponents 官网 高级表格 找到合适的表格复制代码(这里复制第一个)
3.删除不需要组件的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import { EllipsisOutlined, PlusOutlined } from '@ant-design/icons' ;import type { ActionType, ProColumns } from '@ant-design/pro-components' ; } return values; }, }} pagination={{ pageSize: 5 , onChange: (page) => console.log(page), }} dateFormatter="string" headerTitle="高级表格" toolBarRender={() => [ <Button key="button" icon={<PlusOutlined />} onClick={() => { actionRef.current?.reload(); }} type="primary" > 新建 </Button>, <Dropdown key="menu" menu={{ items: [ { label: '1st item' , key: '1' , }, { label: '2nd item' , key: '2' , }, { label: '3rd item' , key: '3' , }, ], }} > <Button> <EllipsisOutlined /> </Button> </Dropdown>, ]} /> ); };
4.api.ts 文件里定义一个 searchUsers 接口(复制 notices 接口,进行修改即可)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export async function searchUsers (options?: { [key: string]: any }) { return request<API.CurrentUser[]>('/api/user/search' , { method: 'GET' , ...(options || {}), }); } export async function getNotices (options?: { [key: string]: any }) { return request<API.NoticeIconList>('/api/notices' , { method: 'GET' , ...(options || {}), }); }
效果如下
8用户注销功能 后端开发 1.业务逻辑层 ◦UserService 中写 userLogout 方法
•需要返回什么参数? 查看登录时的逻辑,用户登陆成功,session 中保存了用户的登录态,反之,注销,移除登录态即可
◦UserServiceImpl 中实现方法
Alt + Enter 选择 Implement methods
确定
生成的代码
▪修改一下代码,获取 session 中的数据,鼠标指示在 removeAttribute 上,发现返回值是 void,修改一下返回值【后续注销失败抛出异常即可,不需要定义一个返回值】,UserService中也修改一下
可以进入 getSession => HttpSession 中查看一下
2.UserController 中编写接口 ◦复制 login 接口的代码进行修改
UserService 返回值修改回 int
UserServiceImpl 返回值修改回 int 注销成功返回 1
3.文件中修改的代码 ◦UserService
1 2 3 4 5 6 7 int userLogout (HttpServletRequest request) ;
◦UserServiceImpl
1 2 3 4 5 6 7 8 9 10 @Override public int userLogout (HttpServletRequest request) { request.getSession().removeAttribute(USER_LOGIN_STATE); return 1 ; }
◦UserController
1 2 3 4 5 6 7 @PostMapping("/logout") public Integer userLogout (HttpServletRequest request) { if (request == null ) { return null ; } return userService.userLogout(request); }
前端开发 1.启动前后端代码(后端:debug、前端:start-dev),登录一个管理员账号 2.修改代码实现用户注销
注销按钮在导航条上,导航条是所有页面都有的组件(components),页面触发注销的位置是头像的下拉菜单(AvatarDropdown),所以我们定位到 src/components/RightContent/AvatarDropdown.tsx 文件
◦找到 退出登录 ,搜索 logout ,找到并进入 loginOut 方法,进入 outLogin 方法,发现进入到 api.ts 文件,修改 outLogin 的接口和后端一致
◦测试一下,点击退出登录,可以看到返回到登陆页面,响应值为 1
3.文件中修改的代码 ◦api.ts
1 2 3 4 5 6 7 export async function outLogin (options?: { [key: string]: any }) { return request<Record<string, any>>('/api/user/logout' , { method: 'POST' , ...(options || {}), }); }
9用户校验
仅适用于用户可信的情况
先让用户自己填:2-5 位星球编号 •后台补充对编号的校验 ◦长度校验 ◦唯一性校验 •前端补充输入框,适配后端
后期拉取星球数据,定期清理违规用户
后端 1.新增 planetCode - 星球编号 字段 ◦数据库添加字段 planetCode - 星球编号
◦User.java 和 UserMapper.xml 中增加 planetCode 字段(直接添加下面代码或者使用 MyBatisX 插件再次生成,将新增字段字段粘到 User 中,修改 UserMapper.xml 文件中引用的包名)
◦UserServiceImpl 中的用户脱敏逻辑也要增加这个字段,将这个字段的数据返回给前端
◦给注册请求体 src/main/java/com/han/usercenter/model/domain/request/UserRegisterRequest.java 增加这个字段的参数
◦ UserController 中增加这个字段
◦UserService 中增加这个字段,加上注释,点击左边的图标,进入下一层(Impl)
◦UserServiceImpl 中增加这个字段,增加对 planetCode 的校验
增加字段,判断 planetCode 长度不能大于 5
校验星球编号不能重复
插入星球编号数据
◦UserServiceTest 中增加这个字段参数
这种方式,需要我们把调用到的地方都进行修改,比较麻烦,但可以让我们有意识的修改之前的逻辑 参数较多时,可以把这些对象进行封装,不用繁琐的修改
2.打断点进行测试一下,失败(因为符合注册条件的账户都注册过了,可以自己新增测试)
3.手动给管理员账号增加一个星球编号
4.文件中修改的代码 ◦User.java
1 2 3 4 private String planetCode;
◦UserMapper.xml【如果选择插件重新生成代码,记得修改紫色部分】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <mapper namespace="com.yupi.usercenter.mapper.UserMapper" > <resultMap id="BaseResultMap" type="com.yupi.usercenter.model.domain.User" > <id property="id" column="id" jdbcType="BIGINT" /> <result property="username" column="username" jdbcType="VARCHAR" /> <result property="userAccount" column="userAccount" jdbcType="VARCHAR" /> <result property="avatarUrl" column="avatarUrl" jdbcType="VARCHAR" /> <result property="gender" column="gender" jdbcType="TINYINT" /> <result property="userPassword" column="userPassword" jdbcType="VARCHAR" /> <result property="phone" column="phone" jdbcType="VARCHAR" /> <result property="email" column="email" jdbcType="VARCHAR" /> <result property="userStatus" column="userStatus" jdbcType="INTEGER" /> <result property="createTime" column="createTime" jdbcType="TIMESTAMP" /> <result property="updateTime" column="updateTime" jdbcType="TIMESTAMP" /> <result property="isDelete" column="isDelete" jdbcType="TINYINT" /> <result property="userRole" column="userRole" jdbcType="INTEGER" /> <result property="planetCode" column="planetCode" jdbcType="VARCHAR" /> </resultMap> <sql id="Base_Column_List" > id,username,userAccount, avatarUrl,gender,userPassword, phone,email,userStatus, createTime,updateTime,isDelete, userRole,planetCode </sql> </mapper>
◦UserServiceIpml.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 @Override public User getSafetyUser (User originUser) { if (originUser == null ) { return null ; } User safetyUser = new User (); safetyUser.setId(originUser.getId()); safetyUser.setUsername(originUser.getUsername()); safetyUser.setUserAccount(originUser.getUserAccount()); safetyUser.setAvatarUrl(originUser.getAvatarUrl()); safetyUser.setGender(originUser.getGender()); safetyUser.setPhone(originUser.getPhone()); safetyUser.setEmail(originUser.getEmail()); safetyUser.setPlanetCode(originUser.getPlanetCode()); safetyUser.setUserRole(originUser.getUserRole()); safetyUser.setUserStatus(originUser.getUserStatus()); safetyUser.setCreateTime(originUser.getCreateTime()); return safetyUser; } @Override public long userRegister (String userAccount, String userPassword, String checkPassword, String planetCode) { if (StringUtils.isAnyBlank(userAccount,userPassword,checkPassword,planetCode)) { return -1 ; } if (userAccount.length() < 4 ){ return -1 ; } if (userPassword.length() < 8 || checkPassword.length() < 8 ){ return -1 ; } if (planetCode.length() > 5 ){ return -1 ; } …… …… QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("userAccount" ,userAccount); long count = userMapper.selectCount(queryWrapper); if (count > 0 ) { return -1 ; } queryWrapper = new QueryWrapper <>(); queryWrapper.eq("planetCode" ,planetCode); count = userMapper.selectCount(queryWrapper); if (count > 0 ) { return -1 ; } …… User user = new User (); user.setUserAccount(userAccount); user.setUserPassword(encryptPassword); user.setPlanetCode(planetCode); boolean saveResult = this .save(user); if (!saveResult) { return -1 ; } return user.getId(); }
◦UserRegisterRequest.java
1 private String planetCode;
◦UsetController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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(); String planetCode = userRegisterRequest.getPlanetCode(); if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword,planetCode)){ return null ; } return userService.userRegister(userAccount, userPassword, checkPassword,planetCode); }
◦UserService.java
1 2 3 4 5 6 7 8 9 10 long userRegister (String userAccount, String userPassword, String checkPassword, String planetCode) ;
◦UserServiceTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Test void userRegister () { String userAccount = "yupi" ; String userPassword = "" ; String checkPassword = "123456" ; String planetCode = "1" ; long result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); Assertions.assertEquals(-1 , result); userAccount = "yu" ; result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); Assertions.assertEquals(-1 , result); userAccount = "yupi" ; userPassword = "123456" ; result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); Assertions.assertEquals(-1 , result); userAccount = "yu pi" ; userPassword = "12345678" ; result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); Assertions.assertEquals(-1 , result); checkPassword = "123456789" ; result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); Assertions.assertEquals(-1 , result); userAccount = "dogYupi" ; checkPassword = "12345678" ; result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); Assertions.assertEquals(-1 , result); userAccount = "yupi" ; result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); Assertions.assertEquals(-1 , result); }
前端
src/pages/user/Register/index.tsx 注册页补充一个输入框 ◦Register/index.ts 复制账号框粘贴到确认密码框后,修改一下
◦进入 RegisterParams (src/services/ant-design-pro/typings.d.ts 文件),增加 planetCode
◦进入 register ,找到 CurrentUser 进入,增加 planetCode
2.src/pages/Admin/UserManage/index.tsx 管理页表格中增加星球编号的列 ◦复制 状态 列,修改一下;可以看到注册页面多了一个星球编号输入框;管理员登陆,出现 星球编号 列
◦新注册一个账号,测试
不填星球编号测试
带星球编号测试
注册成功
跳转到登录页面有返回信息
后端数据库接收到数据
显示星球编号
3.文件中修改代码 ◦Register/index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <ProFormText name="planetCode" fieldProps={{ size: 'large' , prefix: <UserOutlined className={styles.prefixIcon} />, }} placeholder={'请输入星球编号' } rules={[ { required: true , message: '星球编号是必填项!' , }, ]} />
◦typings.d.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 TypeScript type RegisterParams = { userAccount?: string; userPassword?: string; checkPassword?: string; planetCode?: string; type?: string; }; type CurrentUser = { id: number; username: string; userAccount: string; avatarUrl?: string; gender: number; phone: string; email: string; userStatus: number; userRole: number; planetCode: string; createTime: Date; };
◦UserManage/index.tsx
1 2 3 {title: '星球编号' , dataIndex: 'planetCode' , },
10后端代码优化
通用返回对象 ◦目的:给对象补充一些信息,告诉前端这个请求在业务层面是成功还是失败
后端直接返回对象给前端,如果数据出现问题、后端报错、查询不到数据,前端不知道是为什么报错
1 2 3 4 5 6 7 8 { "code" : 0 "data" : { "name" : "yupi" } "message" : "ok" }
1 2 3 4 5 6 { "code" : 50001 "data" : null "message" : "用户异常操作、xxx" }
之前所有异常返回 -1 ,但前端以及后端刚接触这个项目的人不知道 -1 代表什么 ◦自定义错误码 ◦返回类支持返回正常和错误
封装全局异常处理 ◦定义业务异常类 ▪相对于 Java 的异常类,支持更多字段 ▪自定义构造函数,更灵活 / 快捷的设置字段 ◦编写全局异常处理器 ▪作用 •捕获代码中所有的异常,内部消化,集中处理,让前端得到更详细的业务报错 / 信息 •同时屏蔽掉项目框架本身的异常(不暴露服务器内部状态) •集中处理,比如:记录日志 ▪实现 •Spring AOP:在调用方法前后进行额外的处理
TODO 全局请求日志和登录校验
通用反馈对象
com.yupi.usercenter.common 下新建 common 包,在 common 包下新建 BaseResponse.java,编写代码 ◦新建
◦编写如下代码,Alt + Insert 生成 Constructor 方法,复制一份修改一下
编写代码,生成 constructor 方法
选择生成内容
生成内容
复制一份生成的代码,进行修改
将之前所有的请求使用 BaseResponse 封装一下 register、login 接口
但这样很复杂且重复工作多,先去编写一个工具类以及定制一个快捷键 ◦common 包下新建 ResultUtils.java ,编写代码
◦定制一个快捷键 File => Settings => Editor => Live Templates
新建 customJava
在 customJava 下新建一个快捷键
新建快捷键详细信息
测试一下效果
◦UserController 中进行封装 register
login
logout
current
search
delete
文件中修改代码 ◦新建 common.BaseResponse.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.yupi.usercenter.common;import lombok.Data;import java.io.Serializable;@Data public class BaseResponse <T> implements Serializable { private int code; private T data; private String message; public BaseResponse (int code, T data, String message) { this .code = code; this .data = data; this .message = message; } public BaseResponse (int code, T data) { this (code, data, "" ); } }
◦新建 common.ResultUtils.java
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.yupi.usercenter.common;public class ResultUtils { public static <T> BaseResponse<T> success (T data) { return new BaseResponse <>(0 , data, "success" ); } }
◦UserController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 @RestController @RequestMapping("/user") public class UserController { @Resource private UserService userService; @PostMapping("/register") public BaseResponse<Long> userRegister (@RequestBody UserRegisterRequest userRegisterRequest) { if (userRegisterRequest == null ) { return null ; } String userAccount = userRegisterRequest.getUserAccount(); String userPassword = userRegisterRequest.getUserPassword(); String checkPassword = userRegisterRequest.getCheckPassword(); String planetCode = userRegisterRequest.getPlanetCode(); if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword,planetCode)){ return null ; } long result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); return ResultUtils.success(result); } @PostMapping("/login") public BaseResponse<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 ; } User user = userService.userLogin(userAccount, userPassword, request); return ResultUtils.success(user); } @PostMapping("/logout") public BaseResponse<Integer> userLogout (HttpServletRequest request) { if (request == null ) { return null ; } int result = userService.userLogout(request); return ResultUtils.success(result); } @GetMapping("/current") public BaseResponse<User> getCurrentUser (HttpServletRequest request) { Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE); User currentUser = (User) userObj; if (currentUser == null ) { return null ; } Long userId = currentUser.getId(); User user = userService.getById(userId); User safetyUser = userService.getSafetyUser(user); return ResultUtils.success(safetyUser); @GetMapping("/search") public BaseResponse<List<User>> searchUsers (String username,HttpServletRequest request) { if (!isAdmin(request)){ return null ; } QueryWrapper<User> queryWrapper = new QueryWrapper <>(); if (StringUtils.isNotBlank(username)) { queryWrapper.like("username" , username); } List<User> userList = userService.list(queryWrapper); List<User> list = userList.stream().map(user -> userService.getSafetyUser(user)).collect(Collectors.toList()); return ResultUtils.success(list); } @PostMapping("/delete") public BaseResponse<Boolean> deleteUser (@RequestBody long id,HttpServletRequest request) { if (!isAdmin(request)){ return null ; } if (id <= 0 ) { return null ; } boolean b = userService.removeById(id); return ResultUtils.success(b); } private boolean isAdmin (HttpServletRequest request) { Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE); User user = (User) userObj; return user != null && user.getUserRole() == ADMIN_ROLE; } }
自定义异常及错误代码
common 包下新建 ErrorCode.java
新建时选择 Enum
新建 ErrorCode
打上注释
修改一下代码,Alt+Insert 生成 Get 方法
生成 Get 方法
修改 BaseResponse ,完善 ResultUtils
修改 BaseResponse
完善 ResultUtils
3.修改 UserController
•此时 UserController 中多个地方都需要修改,这种修改方式有提示(视觉上不舒服),每次都要调用这个错误类 •定义一个全局异常处理类,捕获整个业务中的异常,并且根据异常信息返回 ResultUtils.error
文件中修改代码 ◦ErrorCode.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.yupi.usercenter.common;public enum ErrorCode { SUCCESS(0 , "success" , "" ), PARAMS_ERROR(4000 , "请求参数错误" , "" ), NULL_ERROR(4001 , "请求参数为空" , "" ), NO_LOGIN(40100 , "未登录" , "" ), NO_AUTH(40101 , "无权限" , "" ); private final int code; private final String message; private final String description; ErrorCode(int code, String message, String description) { this .code = code; this .message = message; this .description = description; } public int getCode () { return code; } public String getMessage () { return message; } }
◦BaseResponse.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Data public class BaseResponse <T> implements Serializable { private int code; private T data; private String message; private String description; public BaseResponse (int code, T data, String message, String description) { this .code = code; this .data = data; this .message = message; this .description = description; } public BaseResponse (int code, T data, String message) { this (code, data, message, "" ); } public BaseResponse (int code, T data) { this (code, data, "" , "" ); } public BaseResponse (ErrorCode errorCode) { this (errorCode.getCode(), null , errorCode.getMessage(), errorCode.getDescription()); } }
◦ResultUtils.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class ResultUtils { public static <T> BaseResponse<T> success (T data) { return new BaseResponse <>(0 , data, "success" ); } public static BaseResponse error (ErrorCode errorCode) { return new BaseResponse <>(errorCode); } }
◦UserController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @PostMapping("/register") public BaseResponse<Long> userRegister (@RequestBody UserRegisterRequest userRegisterRequest) { if (userRegisterRequest == null ) { return ResultUtils.error(ErrorCode.PARAMS_ERROR); } String userAccount = userRegisterRequest.getUserAccount(); String userPassword = userRegisterRequest.getUserPassword(); String checkPassword = userRegisterRequest.getCheckPassword(); String planetCode = userRegisterRequest.getPlanetCode(); if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword,planetCode)){ return null ; } long result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); return ResultUtils.success(result); } @PostMapping("/login") public BaseResponse<User> userLogin (@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) { if (userLoginRequest == null ) { return ResultUtils.error(ErrorCode.PARAMS_ERROR); } String userAccount = userLoginRequest.getUserAccount(); String userPassword = userLoginRequest.getUserPassword(); if (StringUtils.isAnyBlank(userAccount, userPassword)) { return ResultUtils.error(ErrorCode.PARAMS_ERROR); } User user = userService.userLogin(userAccount, userPassword, request); return ResultUtils.success(user); }
全局异常处理器
com.yupi.usercenter 下新建 exception 包,exception 包下新建 BusinessException.java
新建 BuisnessException.java,增加代码,生成 constructor
复制生成的 constructor 方法,修改一下 , 生成 Getter
生成的 Get 方法
增加 final
Debug 方式启动,前端注册进行测试(将星球编号写超过 5 位) ◦显示注册失败,抛出 500,虽然业务抛出异常 HTTP 状态码是 500 ,但是不应该让前端出现 500 ,应该返回刚刚定义的 40000 ◦并且返回了后端的框架结构
定义全局异常处理器 ◦exception 包下新建 GlobalExceptionHandler.java
◦ErrorCode 中增加系统内部异常状态码
◦ResultUtils 中增加三个构造函数
◦GlobalExceptionHandler 中编写具体异常处理代码
修改 UserController
register
search
logout
delete
login
current
修改 UserServiceImpl
还有很多返回值进行自定义处理
再次 Debug 启动,进行注册测试,前端返回想要的数据和提示
💡启动项目报错了 •报错信息:Error creating bean with name ‘handlerExceptionResolver’ defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Bean instantiation via factory method failed; •报错原因:编写 GlobalExceptionHandler 中编写具体异常处理代码 时,两个方法匹配了同一个ExceptionHandler(复制粘贴时忘记更改了,下述代码已更正) •报错解决:将两个方法匹配到对应的 ExceptionHandler 即可 @ExceptionHandler(BusinessException.class) @ExceptionHandler(RuntimeException.class)
7.文件中修改代码 ◦新建 BuisnessException.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.yupi.usercenter.exception;import com.yupi.usercenter.common.ErrorCode;public class BusinessException extends RuntimeException { private final int code; private final String description; public BusinessException (String message, int code, String description) { super (message); this .code = code; this .description = description; } public BusinessException (ErrorCode errorCode) { super (errorCode.getMessage()); this .code = errorCode.getCode(); this .description = errorCode.getDescription(); } public BusinessException (ErrorCode errorCode, String description) { super (errorCode.getMessage()); this .code = errorCode.getCode(); this .description = description; } public int getCode () { return code; } public String getDescription () { return description; } }
◦ErrorCode.javaSYSTEM_ERROR(50000, "系统内部异常", "");
◦ResultUtils.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static BaseResponse error (int code, String message, String description) { return new BaseResponse <>(code, null , message, description); } public static BaseResponse error (ErrorCode errorCode, String message, String description) { return new BaseResponse (errorCode.getCode(), null , message, description); } public static BaseResponse error (ErrorCode errorCode, String description) { return new BaseResponse (errorCode.getCode(), errorCode.getMessage(), description); }
◦GlobalExceptionHandler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.yupi.usercenter.exception;import com.yupi.usercenter.common.BaseResponse;import com.yupi.usercenter.common.ErrorCode;import com.yupi.usercenter.common.ResultUtils;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public BaseResponse businessExceptionHandler (BusinessException e) { log.error("runtimeException" + e.getMessage(), e); return ResultUtils.error(e.getCode(), e.getMessage(), e.getDescription()); } @ExceptionHandler(RuntimeException.class) public BaseResponse runtimeExceptionHandler (Exception e) { log.error("runtimeException" ,e); return ResultUtils.error(ErrorCode.SYSTEM_ERROR,"" ); } }
◦UserController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 @RestController @RequestMapping("/user") public class UserController { @Resource private UserService userService; @PostMapping("/register") public BaseResponse<Long> userRegister (@RequestBody UserRegisterRequest userRegisterRequest) { if (userRegisterRequest == null ) { throw new BusinessException (ErrorCode.PARAMS_ERROR); } String userAccount = userRegisterRequest.getUserAccount(); String userPassword = userRegisterRequest.getUserPassword(); String checkPassword = userRegisterRequest.getCheckPassword(); String planetCode = userRegisterRequest.getPlanetCode(); if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword,planetCode)){ throw new BusinessException (ErrorCode.PARAMS_ERROR); } long result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); return ResultUtils.success(result); } @PostMapping("/login") public BaseResponse<User> userLogin (@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) { if (userLoginRequest == null ) { throw new BusinessException (ErrorCode.PARAMS_ERROR); } String userAccount = userLoginRequest.getUserAccount(); String userPassword = userLoginRequest.getUserPassword(); if (StringUtils.isAnyBlank(userAccount, userPassword)) { throw new BusinessException (ErrorCode.PARAMS_ERROR); } User user = userService.userLogin(userAccount, userPassword, request); return ResultUtils.success(user); } @PostMapping("/logout") public BaseResponse<Integer> userLogout (HttpServletRequest request) { if (request == null ) { throw new BusinessException (ErrorCode.PARAMS_ERROR); } int result = userService.userLogout(request); return ResultUtils.success(result); } String userPassword = userRegisterRequest.getUserPassword(); String checkPassword = userRegisterRequest.getCheckPassword(); String planetCode = userRegisterRequest.getPlanetCode(); if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword,planetCode)){ throw new BusinessException (ErrorCode.PARAMS_ERROR); } long result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode); return ResultUtils.success(result); } @PostMapping("/logout") public BaseResponse<Integer> userLogout (HttpServletRequest request) { if (request == null ) { throw new BusinessException (ErrorCode.PARAMS_ERROR); } int result = userService.userLogout(request); return ResultUtils.success(result); } @GetMapping("/current") public BaseResponse<User> getCurrentUser (HttpServletRequest request) { Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE); User currentUser = (User) userObj; if (currentUser == null ) { throw new BusinessException (ErrorCode.NO_LOGIN); } Long userId = currentUser.getId(); User user = userService.getById(userId); User safetyUser = userService.getSafetyUser(user); return ResultUtils.success(safetyUser); } @GetMapping("/search") public BaseResponse<List<User>> searchUsers (String username,HttpServletRequest request) { if (!isAdmin(request)){ throw new BusinessException (ErrorCode.PARAMS_ERROR); } QueryWrapper<User> queryWrapper = new QueryWrapper <>(); if (StringUtils.isNotBlank(username)) { queryWrapper.like("username" , username); } List<User> userList = userService.list(queryWrapper); List<User> list = userList.stream().map(user -> userService.getSafetyUser(user)).collect(Collectors.toList()); return ResultUtils.success(list); } @PostMapping("/delete") public BaseResponse<Boolean> deleteUser (@RequestBody long id,HttpServletRequest request) { if (!isAdmin(request)){ throw new BusinessException (ErrorCode.NO_AUTH); } if (id <= 0 ) { throw new BusinessException (ErrorCode.PARAMS_ERROR); } boolean b = userService.removeById(id); return ResultUtils.success(b); } private boolean isAdmin (HttpServletRequest request) { Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE); User user = (User) userObj; return user != null && user.getUserRole() == ADMIN_ROLE; } }
◦UserServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Override public long userRegister (String userAccount, String userPassword, String checkPassword, String planetCode) { if (StringUtils.isAnyBlank(userAccount,userPassword,checkPassword,planetCode)) { throw new BusinessException (ErrorCode.PARAMS_ERROR, "参数为空" ); } if (userAccount.length() < 4 ){ throw new BusinessException (ErrorCode.PARAMS_ERROR, "用户账号过短" ); } if (userPassword.length() < 8 || checkPassword.length() < 8 ){ throw new BusinessException (ErrorCode.PARAMS_ERROR, "密码长度过短" ); } if (planetCode.length() > 5 ){ throw new BusinessException (ErrorCode.PARAMS_ERROR, "星球编号过长" ); } …… QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("userAccount" ,userAccount); long count = userMapper.selectCount(queryWrapper);if (count > 0 ) { throw new BusinessException (ErrorCode.PARAMS_ERROR, "用户账号重复" ); } queryWrapper = new QueryWrapper <>(); queryWrapper.eq("planetCode" ,planetCode); count = userMapper.selectCount(queryWrapper); if (count > 0 ) { throw new BusinessException (ErrorCode.PARAMS_ERROR, "星球编号重复" ); }
11前端优化 •对接后端的返回值,取 data •全局响应处理:https://blog.csdn.net/huantai3334/article/details/116780020
◦应用场景:我们需要对接口的 通用响应 进行统一处理,比如从 response 中取出 data ;或者根据 code 去集中处理错误,比如用户未登录、没权限之类的 ◦优势:不用在每个接口请求中都去写相同的逻辑 ◦实现:参考使用的封装工具的官方文档,比如 umi-request ,如果使用 axios,参考 axios 的文档。创建新的文件,在该文件中配置一个全局请求类。在发送请求时,使用自己全局请求类
全局请求响应拦截器封装(一)
定义通用返回对象 为了前后端做适配 ◦src/services/ant-design-pro/typings.d.ts 文件中定义通用返回类 ◦src/services/ant-design-pro/api.ts 文件中给 Register 的响应类型封装一下 ◦src/pages/user/Register/index.tsx 文件中修改一下注册判断 ◦再次注册,可以看到有后端定义的 星球编号过长 的报错提示 ◦更改一些其他 API current logout login search
前端写一个全局响应拦截器 ◦点击 request ,编写代码,测试一下,返回后端传递的真实参数 ◦将所有接口中响应的 data 取出来,继续编写代码
3.文件中修改代码 ◦typings.d.ts
1 2 3 4 5 6 7 8 9 type BaseResponse<T> = { code: number, data: T, message: string, description: string, }
◦api.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 export async function currentUser (options?: { [key: string]: any }) { return request<API.BaseResponse<API.CurrentUser>>('/api/user/current' , { method: 'GET' , ...(options || {}), }); } export async function outLogin (options?: { [key: string]: any }) { return request<API.BaseResponse<number>>('/api/user/logout' , { method: 'POST' , ...(options || {}), }); } export async function login (body: API.LoginParams, options?: { [key: string]: any }) { return request<API.BaseResponse<API.LoginResult>>('/api/user/login' , { method: 'POST' , headers: { 'Content-Type' : 'application/json' , }, data: body, ...(options || {}), }); } export async function register (body: API.RegisterParams, options?: { [key: string]: any }) { return request<API.BaseResponse<API.RegisterResult>>('/api/user/register' , { method: 'POST' , headers: { 'Content-Type' : 'application/json' , }, data: body, ...(options || {}), }); } export async function searchUsers (options?: { [key: string]: any }) { return request<API.BaseResponse<API.CurrentUser[]>>('/api/user/search' , { method: 'GET' , ...(options || {}), }); }
◦index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 try { const res = await register (values) ; if (res.code === 0 && res.data > 0 ) { const defaultLoginSuccessMessage = '注册成功!' ; message.success(defaultLoginSuccessMessage); if (!history) return ; const { query } = history.location; history.push({ pathname: '/user/login' , query, }); return ; } else { throw new Error (res.description); } } catch (error: any) { const defaultLoginFailureMessage = '注册失败,请重试!' ; message.error(error.message ?? defaultLoginFailureMessage); } try { const id = await register (values) ; if (id) { const defaultLoginSuccessMessage = '注册成功!' ; message.success(defaultLoginSuccessMessage); if (!history) return ; const { query } = history.location; history.push({ pathname: '/user/login' , query, }); return ; } message.error(defaultLoginFailureMessage); }
◦request.ts
1 2 3 4 5 6 requestConfig.responseInterceptors = [ async function (response: Response, options: RequestOptionsInit) : Response | Promise<Response> { const data = await response.clone().json(); if () } ]
全局请求响应拦截器封装(二) •request.ts 属于 .umi ,是框架自动生成的,会被覆盖掉,在 .gitignore 添加 .umi ,让编辑器帮我们识别它是项目自动生成的文件 •找回代码
新建请求类,覆盖默认的 request 方法 ◦src 文件夹下新建一个 plugins 文件夹,plugins 文件夹下新建 globalRequests.ts , ◦将 这篇博客 的代码复制进来,删除不需要的代码
◦修改代码 ▪定义一下请求拦截器,比如每次发送请求时在控制台打印一下日志 ▪编辑所有响应拦截器,前端接收到响应后,先获取响应的 data ,然后从这个 data 中判断一下
▪api.ts 中引入自己写的 request ◦注册测试一下,正常提示报错信息 ◦登录测试一下,登陆成功 ◦退出登录,直接访问登录成功后的用户管理页面,可以看到有 请先登录 的提示信息,返回 40100 状态码,重定向到登录页面。完成✌
用户管理页面链接: http://localhost:8000/admin/user-manage?current=1&pageSize=5
12项目部署 多环境理论及实战(一)
多环境参考文章:https://blog.csdn.net/weixin_41701290/article/details/120173283 本地开发:localhost (127.0.0.1)
•多环境:一个项目部署在不同环境上进行调整配置 •为什么需要? ◦为了环境互不影响 ◦区分不同的阶段:开发 / 测试 / 生产 ◦对项目进行优化: ▪本地日志级别 ▪精简依赖,节省项目体积 ▪项目的 环境/参数 可以调整,比如 JVM 参数
•多环境分类 ◦本地环境:localhost,标识 local ◦开发环境:(远程开发)连同一机器,开发方便 dev ◦测试环境:(测试) 开发/测试/产品 使用; 单元测试/性能测试/功能测试/系统集成测试;独立的数据库、独立的服务器 test ◦预发布环境:(体验服)和正式环境一致,正式数据库,更严谨,查出更多问题 pre ◦正式环境:(上线)尽量不改动保证代码“完美”运行 prod ◦沙箱环境:(实验环境):为了做实验遵循 3 个步骤:抽象配置类 + 配置文件化 + 注入环境参数
多环境理论及实战(二)- 前端
请求一个网页时,是先把网页的文件拿到自己的浏览器中,之后网页需要哪个文件再发出请求,localhost 请求自己的电脑,之后部署上线,不能让自己的电脑一直运行,所以我们要修改一下 localhost:8000
请求地址 ◦开发环境:localhost:8000 ◦线上环境:user-backend.code-nav.cn (鱼皮备案过的域名)
启动方式 ◦开发环境:npm run start(本地启动、监听端口、自动更新) ◦线上环境:npm run build(项目构建打包),可以使用 serve 工具启动(npm i -g serve)
serve 安装:管理员方式打开命令行, 输入 npm i -g serve 命令,等待安装,安装后输入 serve -v 查看版本
本地运行一下这个项目 ▪右键 dist 文件夹,选择 Open in ,选择 Terminal ▪输入 serve 命令,访问第一个网址,网页弹出 production 使用 serve 启动相当于在自己电脑上运行了线上环境,自己的电脑就相当于是服务器(别人无法访问)
为什么输出 production? 在 package.json 中执行 build 命令时,umi 框架帮我们把 NODE_ENV = production 传进来了
问题:
这里 serve 出现安装成功但无法使用情况
原因:之前安装 node 根据网上的教程设置了 npm config set prefix 和 npm config set cache 配置,之后使用 nvm 安装 node,没有将配置更改回默认
解决: ◦删除 .npmrc 文件中上述两个配置 或者直接删除文件然后重新配置一下镜像源【查找该文件的位置 cmd 命令行输入如下命令 npm config ls 】,执行 serve 全局安装命令即可 ◦或者试试将之前 prefix 配置的位置增加到环境变量 PATH 中
线上的环境请求线上的后端 ▪找到 src/plugins/globalRequests.ts 全局请求类,对 umi-request 做额外的配置,执行 build,刷新页面
prefix: process.env.NODE_ENV === 'production' ? '[http://user-backend.code-nav.cn'](http://user-backend.code-nav.cn') : undefined
start 启动访问地址是 8000
重新 build ,访问地址是 刚才配置的后端地址
简单了解静态化 静态网页后缀一般为.html或者是.HTM 动态网页不是真实存在的页面,需要执行 ASP,php,asp.net 这样的外语言所生成的一个虚拟的网页,是以.asp、.jsp、.php、.perl、.cgi等形式为后缀,并且在动态网页网址中有一个标志性的符号—“?”
静态化 •根据页面路由,将每个路由都生成一个 index.html 静态文件 •优点:◦网页打开速度快◦相对比较稳定
3.项目的配置 ◦不同的项目(框架)都有不同的配置文件,umi 的配置文件是 config可以在配置文件后添加对应的环境名称后缀来区分开发环境和生产环境 ▪开发环境:config.dev.ts ▪生产环境:config.pro.ts ▪公共配置:config.ts 不带后缀 Umi 部署文档:https://v3.umijs.org/zh-CN/docs/deployment
多环境理论及实战(三)- 后端
主要是改 •依赖的环境地址 ◦数据库地址 ◦缓存地址 ◦消息队列地址 ◦项目端口号 •服务器配置
SpringBoot 项目,通过 application.yml 添加不同的后缀来区分配置文件【可以在项目启动时传入环境变量】 application.yml 是公共配置,任何环境都会加载这个配置,通用的配置(例如 mybatis-plus 的配置),只写在公共配置即可
找到数据表,右键 => Navigation => Go to DDL,新建 sql 文件夹,sql 文件夹下新建 create_table.sql ,将 DDL 中的建表语句粘贴到新建的 .sql 文件中【如果这个数据表不能开源给其他人看,记得不要给出这个 sql 文件】
线上数据库搭建 Idea 测试连接 远程服务器公网 IP 工作台 => 实例与镜像 => 实例 => 实例详情 => 配置信息 => 公网 IP
线上数据库连接成功后配置 ◦进入线上数据库控制台,创建数据库、数据表 ◦修改 application-prod.yml 数据库配置信息 ◦测试本地运行生产环境的项目
原始 Nginx + SpringBoot 参考文章:https://www.bilibili.com/read/cv16179200 -by 鱼皮 需要 Linux 服务器(建议用 CentOS 8+/7.6 以上) 原始部署:什么都自己装
前端 阿里云在线可视化工具:https://ecs-workbench.aliyun.com/ 需要 web 服务器:nginx、apache、tomcat
安装 nginx 服务器 •用系统自带带软件包管理器快速安装,比如 centos 的 yum •自己到官网安装【参考文章 nginx 下载】
依次执行下列命令
1 2 3 4 5 6 7 8 9 10 11 12 # 查看当前所在目录 pwd /root # 创建 services 目录,存放项目的依赖和安装包 mkdir services # 进入 services 目录 cd services # 列出当前目录所含文件 ls
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # nginx 1.26.2 安装链接 https://nginx.org/download/nginx-1.26.2.tar.gz # 安装 nginx 命令 curl -o nginx-1.26.2.tar.gz https://nginx.org/download/nginx-1.26.2.tar.gz ls # 解压 nginx 安装包 tar -zxvf nginx-1.26.2.tar.gz # 进入 nginx 目录 cd nginx-1.26.2 ls # 删除 nginx 安装包——附加不执行 rm -rf nginx-1.26.2 rm -rf nginx-1.26.2.tar.gz nginx-1.26.2.tar.gz
1 2 3 4 5 6 7 8 9 10 11 12 # 检查 nginx 依赖环境是否正常 ./configure # 安装依赖 yum install pcre pcre-devel -y yum install openssl openssl-devel -y # 设置系统配置参数 ./configure --with-http_ssl_module --with-http_v2_module --with-stream # 再次检查 nginx 依赖环境是否正常 ./configure
1 2 3 4 5 6 7 8 9 # 开始编译 make # 安装 make install ls ls /usr/local/nginx/sbin/nginx # history 命令可以得到所有执行过的命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 配置环境变量 vim /etc/profile # shift + g 跳至最后一行 按 o,最后一行插入export PATH=$PATH:/usr/local/nginx/sbin # 按下 esc ,输入 :wq 回车,保存并退出 # 使文件生效 source /etc/profile # 执行 nginx # 查看启动情况,可以看到 80 端口已被 nginx 占用 netstat -ntlp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 进入 conf 文件 cd conf ls # 复制 nginx.conf 文件为 nginx.default.conf cp nginx.conf nginx.default.conf ls # 查看原始配置文件 cat nginx.conf # 回到 services 目录 cd .. cd .. # 或者 cd /root/services
build 前端项目,将 dist 文件夹直接拖过来,失败了 将 dist 文件夹下的所有文件移动到 user-center-front 文件夹下,删除 dist 文件夹 再次访问,403 了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 查看 nignx 进程启动人 ps -ef | grep nginx # 修改 nginx 启动人为 root vim nginx.conf # 按 i 进入编辑模式,修改如下,修改完成按 esc ,输入 :wq ,回车 user root; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; # 更新配置 nginx -s reload
再次访问,访问成功,结束