鱼皮用户中心开发笔记
企业做项目流程
需求分析 => 设计(概要设计、详细设计等) => 技术选型(对于一个新项目来说是最重要的) =>新项目初始化、老项目引入需要的技术 => 写 Demo =>写代码(实现业务逻辑) => 单元测试 => 代码提交/代码评审 => 部署 => 发布
需求分析
- 登录/注册
- 用户管理(仅管理员可见):对用户的查询或者修改
- 用户校验(仅部分付费用户可注册)
技术选型
前端:
- HTML + CSS + JavaScript
- React
- Ant Design 组件库
- Umi 开发框架
- Ant Design Pro (现成的管理系统)
后端:
- Java
- Spring + SpringMVC +SpringBoot
- MyBatis + MyBatis-Plus
- MySql 数据库
- JUnit 单元测试
部署:服务器/容器(平台)
计划一
- 初始化项目
- 前端初始化 ✔
- 初始化项目 ✔
- 引入组件等 ✔
- 框架瘦身(将没用的代码删除)✔
- 后端初始化
- 准备环境( MySQL 等)✔
- 引入框架/整合框架 ✔
- 前端初始化 ✔
- 登录/注册
- 前端
- 后端
- 用户管理
- 前端
- 后端
初始化项目
一、前端初始化
-
下载node.js
-
最好使用大于等于14的版本,12版本有点老
-
安装后进行换源
npm config get registry #get命令查看registry npm config set registry http://registry.npm.taobao.org #用set换为阿里镜像
-
安装yarn
- 尽量不使用npm命令安装,推荐使用msi安装包安装,下载地址:https://link.csdn.net/?target=https%3A%2F%2Frepo.huaweicloud.com%2Fyarn%2Fv1.22.4%2Fyarn-1.22.4.msi
- 安装 yarn 后需要检查环境配置和安装路径是否相同,否则会报错,找不到该命令
- 通过
yarn -v
验证安装是否成功 - 安装好后还需要进⾏最后⼀步:改变镜像 运行下面的命令即可
yarn config set registry https://registry.npm.taobao.org -g yarn config set sass_binary_site http://cdn.npm.taobao.org/dist/node-sass-g
-
初始化 Ant Design Pro 脚⼿架(关于更多可进入官网了解)
- 创建项目目录,在该目录下打开 cmd ,运行以下命令:(由于 Ant Design Pro 更新 旧版命令可能会有错误,要根据官网初始化步骤进行初始化)
```powershell npm i @ant-design/pro-cli -g pro create myapp ```
-
选择 umi 的版本 一定要选择umi@3
-
选择选择简单的(Simple)脚手架
-
使用 WebStorm 打开项目
-
执行左下角提示的
npm install
/在终端运行 yarn -
依赖下载完成之后即可运行
-
开启Umi UI插件(可帮助我们快速开发完整的界面)
-
安装 Umi UI,控制台运行下面的命令:
yarn add @umijs/preset-ui -D #在项目文件夹中运行! #或 npm install --save-dev @umijs/preset-ui
-
Umi UI 区块无法预览解决方式:在 hosts 里加 151.101.64.133 raw.githubusercontent.com
这个 ip 指向的是美国,其实也不影响功能
-
-
-
项目瘦身
-
移除国际化
-
package.json 已经定义好了移除国际化的命令:
"i18n-remove": "pro i18n-remove --locale=zh-CN --write"
-
运行该脚本
-
删除 src 下的 locales 文件夹
-
-
删除无用的文件
如果删除后无法运行,全局搜索查看哪里引用了该文件,删除该引用即可
-
二、后端初始化
-
初始化MySQL
- 使用 MySQL 5.7 版本(8.0版本可能与服务器不兼容)
- 验证 MySQL是否安装成功 - 使用 IDEA 连接
-
三种方式创建 SpringBoot 项目的方式:
- 在 GitHub 拉取现成的模板:SpringBoot-Templates(不建议从零开始写一个项目或在企业中做项目的情况下使用)
- SpringBoot 官方的模板生成器
- 直接在 IDEA 开发工具中生成 ⭐
-
这里使用在 IDEA 中创建 SpringBoot 项目
-
new => project => Spring Initializr => 填写项目的相关信息(本项目使用 Java8 版本)
-
勾选 Dependencies
- Lombok(注解工具,生成 Get、Set方法等)
- Spring Boot DevTools(热部署)
- Spring Configuration Processor (可以支持 Spring 的ConfigrationProperties 等注解,读取属性文件时使用)
- MySQL Driver (MySQL 驱动)
- Spring Web (增加 Web 访问能力)
- Mybatis Framework (持久层框架)
-
对于缺失的 Dependencies 可以去Maven中心仓库查询
-
导入Junit
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency>
-
引入 MyBatis-Plus
-
导入依赖
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency>
-
在 Spring Boot 启动类中添加
@MapperScan
注解,扫描 Mapper 文件夹@SpringBootApplication @MapperScan("com.mark.usercenter.mapper") public class UserCenterApplication { public static void main(String[] args) { SpringApplication.run(UserCenterApplication.class, args); } }
-
创建数据库
DROP TABLE IF EXISTS user; CREATE TABLE user ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT(11) NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id) ); 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');
-
配置数据源
spring: application: name: user-center datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/universe username: root password: 123456
-
model 包下创建实体类
User.java
@Data public class User { private Long id; private String name; private Integer age; private String email; }
-
编写 Mapper 包下的
UserMapper
接口public interface UserMapper extends BaseMapper<User> { }
-
测试 MyBatis-Plus 导入成功:
@SpringBootTest //这里使用的@Test注解与SpirngBoot并无联系,因此此处需要加RunWith注解 @RunWith(SpringRunner.class) public class SampleTest { @Resource private UserMapper userMapper; @Test public void testSelect() { System.out.println(("----- selectAll method test ------")); //调用Mapper方法,查询条件为空,即查询所有 List<User> userList = userMapper.selectList(null); //断言——预期结果 对比5与查询结果是否相同 Assert.assertEquals(5, userList.size()); //输出 userList.forEach(System.out::println); }
-
-
计划二
- 用户表设计 ✔
- 完成登录注册的前后端开发 ✔
- 获取用户的登录态,获取当前登录用户
- 完成用户管理后台的前后端开发 ✔
一、数据库设计
- 什么是数据库? 存数据的库。
- 数据库中有什么? 数据表(理解为 Excel 表格)
- Java 操作数据库指的是什么? 代替人工操作数据
- 什么是设计数据库表? 根据需求场景,思考有那些表(模型),表中有哪些字段,字段的类型,数据库字段添加索引
- 表与表之间的关联
用户表字段
- id 主键 bigint
- username 昵称 varchar
- userAccount 登录账号 varchar
- avatarUrl 头像 varchar
- gender 性别 tinyint
- userPassword 密码 varchar
- phone 电话 varchar
- email 邮箱 varchar
- userStatus 用户状态 int 0表示正常
- userRole 用户角色 int 0 -> 普通用户 1 -> 管理员
-
createTime 创建时间(数据插入时间) datetime
-
updateTime 更新时间(数据更新时间) datetime
-
isDelete 是否删除(逻辑删除) tinyint
创建表
使用 IDEA 傻瓜式生成数据表
updateTime 和 createTime 需要手动指定,生成并手动修改的 DDL 如下
-- auto-generated definition
create table user
(
id bigint auto_increment comment '主键id'
primary key,
username varchar(256) null comment '用户昵称',
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 comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除'
)
comment '用户';
二、后端
登录注册后端
-
规整项目目录
-
实现基本的数据库操作(操作 user 表)
- 模型 user 对象 => 和数据库的字段关联,自动生成
- 使用 MyBatisX 插件自动生成 domain 实体对象、mapper (操作数据库对象) 、mapper.xml (定义了mapper对象和数据库关联,可以在里面自己写 SQL )、service(包含常用的增删改查)、serviceImpl( service 实现类)
- 右键表 => MyBatisX-Generator => Next => 勾选 MyBatis-Plus3 => 勾选 Comment(注释)、Actual Column(每一个列的实际列名)、 Model(生成 domain ) 、 Lombok(使用 Lombok 注解) => 勾选 mybatis-plus3 => Finish
- 模型 user 对象 => 和数据库的字段关联,自动生成
-
写注册和登录的逻辑
-
注册逻辑:
-
用户在前端输入账号和密码、以及校验码(todo)
-
后端校验用户的账号、密码、校验密码是否符合要求
- 非空
- 账号不小于 4 位
- 密码不小于 8 位
- 账号不能重复
- 账号不包含特殊字符
- 密码和校验密码相同
-
-
对密码进行加密(密码千万不要明文存储在数据库中!!!)
-
向数据库插入用户数据
-
返回用户 id
-
-
完成注册业务层代码
用户服务实现类注册接口:(书写标准的注释)
/** * @author Mark * @description 用户服务 * @createDate 2022-10-20 10:52:05 */ public interface UserService extends IService<User> { /** * 用户注册 * * @param userAccount 用户账户 * @param userPassword 用户密码 * @param checkPassword 校验密码 * @return 新用户id */ long userRegister(String userAccount, String userPassword, String checkPassword); }
service 实现类实现注册:
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { /** * @param userAccount 用户账户 * @param userPassword 用户密码 * @param checkPassword 校验密码 * @return */ @Override public long userRegister(String userAccount, String userPassword, String checkPassword) { // 1. 校验 // 1.1 校验是否为空 引入apache common utils :Apache Commons Lang 使用其中的方法:isAnyBlank 可判断多个字符串是否为空 if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) { // TODO 修改为自定义异常 return -1; } // 1.2 校验账号位数 if (userAccount.length() < 4) { return -1; } // 1.3 校验密码位数 if (userPassword.length() < 8 || checkPassword.length() < 8) { return -1; } // 1.5 校验账号不包含特殊字符 /* pP和pS匹配特殊符号 \s+是空格一个或者多个,不管在那个位置都能匹配 \pP 其中的小写 p 是 property 的意思,表示 Unicode 属性,用于 Unicode 正表达式的前缀。 大写 P 表示 Unicode 字符集七个字符属性之一:标点字符。 大写 P 表示 符号(比如数学符号、货币符号等) */ String validPattern = "[\\u00A0\\s\"`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“'。,、?]"; Matcher matcher = Pattern.compile(validPattern).matcher(userAccount); if (matcher.find()) { return -1; } // 1.6 校验密码和校验密码相同 if (!userPassword.equals(checkPassword)) { return -1; } // 1.4 校验账号不能重复 放在最后 当其他校验通过时 再去数据库查询账号是否存在 防止性能浪费 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserAccount, userAccount); long count = baseMapper.selectCount(queryWrapper); if (count > 0) { return -1; } // 2. 密码加密 final String SALT = "mark"; //使用Spring的加密方法,采用MD5加密方式 String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes(StandardCharsets.UTF_8)); // 3. 向用户数据库插入数据 User user = new User(); user.setUserAccount(userAccount); user.setUserPassword(encryptPassword); boolean saveResult = this.save(user); if (!saveResult) { return -1; } return user.getId(); } }
-
测试注册功能
@Test void userRegister() { //测试密码为空 String userAccount = "mark"; String userPassword = ""; String checkPassword = "12345678"; long result = userService.userRegister(userAccount, userPassword, checkPassword); Assert.assertEquals(-1, result); //测试账号小于4位 userAccount = "la"; userPassword = "12345678"; result = userService.userRegister(userAccount, userPassword, checkPassword); Assert.assertEquals(-1, result); //测试密码小于8位 userAccount = "mark"; userPassword = "123456"; checkPassword = "123456"; result = userService.userRegister(userAccount, userPassword, checkPassword); Assert.assertEquals(-1, result); //测试账号含有特殊字符 userAccount = "mark@ :;"; result = userService.userRegister(userAccount, userPassword, checkPassword); Assert.assertEquals(-1, result); //测试密码和校验密码不相同 userAccount = "mark"; userPassword = "12345678"; checkPassword = "1234568889910"; result = userService.userRegister(userAccount, userPassword, checkPassword); Assert.assertEquals(-1, result); //测试账户名重复 userAccount = "CatMark"; userPassword = "12345678"; checkPassword = "12345678"; result = userService.userRegister(userAccount, userPassword, checkPassword); Assert.assertEquals(-1, result); //测试是否可以注册成功 userAccount = "mark"; userPassword = "12345678"; checkPassword = "12345678"; result = userService.userRegister(userAccount, userPassword, checkPassword); Assert.assertTrue(result > 0); }
-
登录逻辑:
-
接收参数:用户账号、密码
请求类型:POST
请求参数很长时不建议使用get
请求体:JSON 格式的数据
返回值:用户信息(脱敏)
-
逻辑:
- 校验用户账户和密码是否合法
- 非空
- 账号不小于 4 位
- 密码不小于 8 位
- 账号不包含特殊字符
- 校验密码是否输入正确,要和数据库中的密文密码去对比
- 用户信息脱敏,隐藏敏感信息,防止数据库中的字段现楼
- 记录用户的登录态( Session ),将其存到服务器上(用后端 SpringBoot 框架封装的服务器 Tomcat 记录)
- 如何知道哪个用户登录?
- 连接服务器端后,得到一个 session 状态(匿名会话),返回给前端
- 登录成功后,得到了登录成功的 session ,并且给该session设置一些值(比如用户信息),返回给前端一个设置 cookie 的“命令”
- 前端接收到后端的命令后设置 cookie,保存到浏览器内
- 前端再次请求后端的时候(相同的域名),在请求头中带上 cookie 去请求
- 后端拿到前端传来的 cookie ,找到对应的 session
- 后端从 session 中可以取出基于该 session 存储的变量(用户的登录信息、登录名)
- 如何知道哪个用户登录?
- 返回脱敏后的用户信息
- 校验用户账户和密码是否合法
-
-
完成登录业务层代码
用户服务实现类登录接口:
/** * 用户登录态键 */ public static final String USER_LOGIN_STATE = "userLoginState"; /** * 用户登录 * * @param userAccount 用户账户 * @param userPassword 用户密码 * @param request 请求 * @return 脱敏后的用户信息 */ User doLogin(String userAccount, String userPassword, HttpServletRequest request);
service 实现类实现登录:
/** * 用户登录 * * @param userAccount 用户账户 * @param userPassword 用户密码 * @param request 请求 * @return 脱敏后的用户信息 */ @Override public User doLogin(String userAccount, String userPassword, HttpServletRequest request) { // 1. 校验 // 1.1 校验是否为空 if (StringUtils.isAnyBlank(userAccount, userPassword)) { // TODO 修改为自定义异常 return null; } // 1.2 校验账号位数 if (userAccount.length() < 4) { return null; } // 1.3 校验密码位数 if (userPassword.length() < 8) { return null; } // 1.5 校验账号不包含特殊字符 String validPattern = "[\\u00A0\\s\"`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“'。,、?]"; Matcher matcher = Pattern.compile(validPattern).matcher(userAccount); if (matcher.find()) { return null; } // 2. 校验密码是否输入正确,和数据库中的密文密码去对比 String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes(StandardCharsets.UTF_8)); //查询用户是否存在且密码与之匹配 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserAccount, userAccount) .eq(User::getUserPassword, encryptPassword); //这里需要添加逻辑删除 以防查询到已删除的 逻辑删除使用方法:①配置yml ②在实体类字段添加@TableLogic User user = baseMapper.selectOne(queryWrapper); //如果用户不存在 if (user == null) { log.info("user long failed, userAccount cannot match userPassword"); return null; } // 3. 用户脱敏 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()); // 4. 记录用户登录态 request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser); return safetyUser; }
添加逻辑删除:
①配置
com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig
mybatis-plus: global-config: db-config: # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) logic-delete-field: flag # 逻辑已删除值(默认为 1) logic-delete-value: 1 # 逻辑未删除值(默认为 0) logic-not-delete-value: 0
②实体类字段上加上
@TableLogic
注解/** * 是否删除 */ @TableLogic private Integer isDelete;
-
实现登录注册的接口(Controller)
-
创建 UserController 类,添加 @RestController 注解(适用于restful风格的API,返回值默认为json类型),添加 @RequestMapping 注解(请求的路径)
/** * @description 用户接口 用户控制器 * @author Mark * @date 2022/10/20 18:40 * @version 1.0 */ @RestController @RequestMapping("/user") public class UserController { }
-
注册接口
使用一个实体类 UserRegisterRequest 接收前端数据:在 model.domain.request 下创建实体类
@Data public class UserRegisterRequest implements Serializable { /** * 防止序列化过程中冲突 */ private static final long serialVersionUID = 4058009977548376535L; /** * 登录账号 */ private String userAccount; /** * 密码 */ private String userPassword; /** * 校验密码 */ private String checkPassword; }
@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(); //controller层倾向于对请求参数本身的校验,不涉及业务逻辑(越少越好) //service层是对业务逻辑的校验(有可能被controller之外的类调用) if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)){ return null; } long id = userService.userRegister(userAccount, userPassword, checkPassword); return id; }
-
登录接口
使用一个实体类 UserLoginRequest 接收前端数据:在 model.domain.request 下创建实体类
@Data public class UserLoginRequest implements Serializable { /** * 防止序列化过程中冲突 */ private static final long serialVersionUID = -2526169836883035291L; /** * 登录账号 */ private String userAccount; /** * 密码 */ private String userPassword; }
@PostMapping("/login") public User userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) { if (userLoginRequest == null) { return null; } String userAccount = userLoginRequest.getUserAccount(); String userPassword = userLoginRequest.getUserPassword(); //controller层倾向于对请求参数本身的校验,不涉及业务逻辑(越少越好) //service层是对业务逻辑的校验(有可能被controller之外的类调用) if (StringUtils.isAnyBlank(userAccount, userPassword)) { return null; } return userService.userLogin(userAccount, userPassword, request); }
-
接口测试
-
使用 IDEA 自带的测试工具(或者点 Controller 访问路径旁边的小图标也可以)
-
Tools => HTTP Client => Create Request in HTTP Client => Add request
-
书写请求:
POST http://localhost:8010/user/login Content-Type: application/json { "userAccount": "mark", "userPassword": 12345678 }
-
debug启动Application,启动请求debug即可
-
-
获取当前用户登录态
添加接口,获得当前登录用户信息
用户管理后台后端
-
查询用户
-
允许根据用户名查询
控制层
@GetMapping("/search") public List<User> searchUsers(String username) { return userService.searchUsers(username); }
业务层
/** * @param username 用户名 * @return 用户列表 * @description 根据用户名查询用户 */ @Override public List<User> searchUsers(String username) { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); if (StringUtils.isNotBlank(username)) { queryWrapper.like(User::getUsername, username); } return this.list(queryWrapper); }
-
-
删除用户
@PostMapping("/delete") public boolean deleteUser(@RequestBody long id) { if (id <= 0) { return false; } return userService.removeById(id); }
必须鉴权!!!
给user表添加角色字段:右键表 => Modify Table => add 添加字段 userRole,类型为 int,默认值为 0,普通用户,1 为管理员,非空
重新生成domain和mapper.xml,将userRole字段添加到User实体类中,并在设置登录态处添加userRole字段到safetyUser返回
新建包contant,新建UserContant接口(接口中的属性默认为public static),将常量都迁移和写道这里
public interface UserConstant {
/**
* 用户登录态键
*/
String USER_LOGIN_STATE = "userLoginState";
// ---------- 权限 -----------
/**
* 默认权限
*/
int DEFALUT_ROLE = 0;
/**
* 管理员权限
*/
int ADMIN_ROLE = 1;
}
修改业务层和控制层代码,添加判断
@GetMapping("/search")
public List<User> searchUsers(String username, HttpServletRequest request) {
return userService.searchUsers(username, request);
}
@Override
public List<User> searchUsers(String username, HttpServletRequest request) {
//鉴权:仅管理员可查询
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User user = (User) userObj;
if (user == null || user.getUserRole() != ADMIN_ROLE){
return new ArrayList<>();
}
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
queryWrapper.like(User::getUsername, username);
}
return this.list(queryWrapper);
}
@PostMapping("/delete")
public boolean deleteUser(@RequestBody long id, HttpServletRequest request) {
//鉴权:仅管理员可查询
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User user = (User) userObj;
if (user == null || user.getUserRole() != ADMIN_ROLE) {
return false;
}
if (id <= 0) {
return false;
}
return userService.removeById(id);
}
由于有重复代码,所以可提取方法
/**
* 鉴权函数:判断是否为管理员 仅管理员可查询、删除
*
* @param request 请求
* @return 是否为管理员
*/
@Override
public boolean isAdmin(HttpServletRequest request) {
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User user = (User) userObj;
if (user == null || user.getUserRole() != ADMIN_ROLE) {
return false;
}
return true;
}
设置登录态超时时间:
spring:
#session失效时间:一天
session:
timeout: 86400
测试发现请求将密码返回,解决方案:
@Override
public List<User> searchUsers(String username, HttpServletRequest request) {
...
//流式处理,遍历流,将流的password设置为null,返回user再拼接为List
return userList.stream().map(user -> {
user.setUserPassword(null);
return user;
}).collect(Collectors.toList());
}
并且将脱敏提取成方法
/**
* 用户脱敏
*
* @param originUser 完成用户信息
* @return 脱敏后的用户信息
*/
@Override
public User getSafetyUser(User originUser){
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.setUserRole(originUser.getUserRole());
safetyUser.setUserStatus(originUser.getUserStatus());
safetyUser.setCreateTime(originUser.getCreateTime());
return safetyUser;
}
优化查询脱敏:
@Override
public List<User> searchUsers(String username, HttpServletRequest request) {
...
return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
}
后端写代码流程总结
- 先做设计
- 代码实现
- 持续优化!!!(复用代码、提取公共逻辑/常量)
三、前端
Ant Design Pro(Umi 框架)
app.tsx 项目全局入口文件,定义了整个项目中使用的公共数据(比如用户信息)
access.ts 控制用户的访问权限
首次访问页面(刷新页面),进入 app.tsx,执行 getInitialState 方法,该方法的返回值就是全局可用的状态值。
ProComponents 高级表单
- 通过 columns 定义表格有哪些列
- columns 属性
- dataIndex 对应返回数据对象的属性
- title 表格列名
- copyable 是否允许复制
- ellipsis 是否允许缩略
- valueType:用于声明这一列的类型(dateTime、select)
MFSU:前端编译优化
Ant Design 组件库 => React
Ant Design Procomponents => Ant Design
Ant Design Pro 后台管理系统 => Ant Design、React、Ant Design Procomponents、其他的库
登录注册前端
-
修改页脚信息
-
修改登录页面信息
-
删除登录页面不需要的代码
-
修改登录发送的请求
-
修改请求地址:
- 方式一: 在请求前定义常量,请求路径前加上该常量即可
const BASE_PREFIX = process.env ? 'http://localhost:8010' : 'https://xxx';
- 方式二: 通过查看Ant Design Pro官网文档发现,可以在app.tsx全局配置中定义请求地址前缀
export const request: RequestConfig = { timeout: 1000, prefix: 'http://localhost:8010' };
-
前后端端口不同,需要解决跨域问题
-
搭建代理
-
什么是代理?
正向代理:替客户端向服务器发送请求
反向代理:替服务器接收请求。假如有三台服务器,
怎么做代理:Nginx服务器
-
打开 proxy.ts ,Ant Design Pro已经为我们在本地测试环境中搭建了代理
export default { dev: { // localhost:8000/api/** -> http://localhost:8010** '/api': { // 要代理的地址 target: 'http://localhost:8010', // 配置了这个可以从 http 代理到 https // 依赖 origin 的功能可能需要这个,比如 cookie changeOrigin: true, }, } }
-
全局配置中修改全局配置prefix
export const request: RequestConfig = { timeout: 8000, };
-
在发送请求的URL中加/api
/** 登录接口 POST /api/login/account */ export async function login(body: API.LoginParams, options?: { [key: string]: any }) { return request<API.LoginResult>('/api/user/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, data: body, ...(options || {}), }); }
-
修改登录方法以及登录参数发送的参数
try { // 登录 const user = await login({...values, type}); if (user) { const defaultLoginSuccessMessage = intl.formatMessage({ id: 'pages.login.success', defaultMessage: '登录成功!', }); message.success(defaultLoginSuccessMessage); await fetchUserInfo(); /** 此方法会跳转到 redirect 参数所在的位置 */ if (!history) return; const {query} = history.location; const {redirect} = query as { redirect: string }; history.push(redirect || '/'); return; } // 如果失败去设置用户错误信息 setUserLoginState(user);
-
后端支持
server: port: 8010 servlet: context-path: /api
-
-
后端支持跨域(并不够安全,而且要修改后端)
-
-
-
完成注册
复制登陆页面 修改即可
用户管理后台前端
根据用户身份不同加载当前页面的组件 终 2:09 Admin.tsx中添加子组件
使用Ant Design Pro Components 高级表格 searchUsers()方法请求后端
删除一些不需要的列 修改列名
计划三
- 开发用户注销前后端
- 补充用户注册校验逻辑前后端
- 后端代码优化
- 前端代码优化
用户注销
去除Session中的登录态即可
@Override
public boolean userLogout(HttpServletRequest request) {
//移除Session中的登录态
request.getSession().removeAttribute(USER_LOGIN_STATE);
return true;
}
@PostMapping("/logout")
public Boolean userLogout(HttpServletRequest request) {
if (request == null) {
return null;
}
return userService.userLogout(request);
}
修改前端请求地址,退出登录后将页面跳转到登录页面
用户校验
仅适用于用户可信的情况
后天补充对编号的校验:长度校验、唯一性校验
前端补充输入框,适配后端
修改前后端代码
后端优化
通用返回对象
目的:给对象补充扩展一些信息,告诉前端这个请求在业务层面上成功还是失败
定义通用返回类
@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,"");
}
}
定义返回工具类
public class ResultUtils {
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "ok");
}
}
修改Controller方法的返回值
定义错误码类
public enum ErrorCode {
/**
* 成功
*/
SUCCESS(0, "ok", "成功!"),
/**
* 请求参数错误
*/
PARAMS_ERROR(40000, "请求参数错误", ""),
/**
* 请求数据为空
*/
NULL_ERROR(40001, "请求数据为空", ""),
/**
* 未登录
*/
NOT_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;
}
public String getDescription() {
return description;
}
}
修改通用返回类
@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());
}
}
返回工具类添加失败方法,支持返回正常和错误
public class ResultUtils {
/**
* 成功
*
* @param data 数据
* @param <T> 泛型
* @return 通用返回类-成功
*/
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "ok", "");
}
/**
* 失败
*
* @param errorCode 错误码
* @return 通用返回类-失败
*/
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse(errorCode);
}
}
封装全局异常处理
-
定义业务异常类
- 相对于java的异常类,支持更多字段
- 自定义构造函数,更灵活、快捷的设置字段
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; } }
-
编写全局异常处理器
目前出现异常前端会报500,因此需要全局异常处理器
- 捕获代码中所有的异常,内部消化,让前端得到更加详细的业务报错 / 信息
- 同时屏蔽掉项目框架本身的异常(不暴露服务器内部的状态)
- 集中处理,比如集中记录日志进行处理
实现:
- 利用Spring AOP,在调用方法前后进行额外的处理
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 当前方法只取捕获BusinessException异常 * * @return */ @ExceptionHandler(BusinessException.class) public BaseResponse businessExceptionHandler(BusinessException e) { log.error("businessException" + e.getMessage(), e); return ResultUtils.error(e.getCode(), e.getMessage(), e.getDescription()); } @ExceptionHandler(RuntimeException.class) public BaseResponse businessExceptionHandler(RuntimeException e) { log.error("runtimeException", e); return ResultUtils.error(ErrorCode.SYSTEM_ERROR, e.getMessage(), ""); } }
计划四
- 多环境
- 项目部署上线
- 原始前端、后端项目部署
- 宝塔Linux
- 容器
- 容器平台
- 上线后前后端的联调
- 项目扩展和规划
多环境开发
同一套项目代码在不同的阶段需要根据实际情况调节配置并部署到不同的机器上
为什么需要?
- 每个环节互不影响
- 为了区分不同的阶段:开发、测试、生产
- 对项目进行优化
- 本地日志级别
- 精简依赖(节省项目体积)
- 项目的环境/参数可以调整,比如JVM参数
针对不同环境做不同的事情。
多环境分类:
- 本地开发(自己的电脑):localhost(127.0.0.1)
- 开发环境(远程开发):大家连同一台机器,为了大家开发方便
- 测试环境(测试):开发/测试/产品人员使用,单元测试/性能测试/功能测试/系统集成测试,独立的数据库、独立的服务器
- 沙箱环境(实验环境):完全隔离的环境,为了做实验,测试某个功能
- 预发布环境(类似内测/体验服):基本和正式环境一致,使用正式数据库,更严谨、查出更多问题
- 正式环境(线上、公开对外访问的项目):尽量不要改动,保证上线前的代码是“完美”运行的
前端多环境实战
-
请求地址
- 开发环境:localhost:8000
- 线上环境:购买的域名地址
startFront(env){ if(env === 'prod'){ //不输出注释 //项目优化 //修改请求地址 }else{ //保持本地开发逻辑 } }
使用umi框架,在build时会自动传入NODE_ENV == production 参数,start的NODE_ENV参数为app.tsx中的
const isDev = process.env.NODE_ENV === 'development';
的development -
启动方式
-
开发环境:npm run start (本地启动,监听端口,自动更新)
-
线上环境:npm run build(项目构建打包),使用serve工具启动
指定biuld后,可以右键生成的dist目录,在控制台打开,执行serve命令,即可快速启动前端项目
-
-
项目的配置
不同的项目(框架)都有不同的配置文件,umi 的配置文件是 config,可以在配置文件后添加对应的环境名称后缀来区分开发环境和生产环境。参考文档:https://umijs.org/zh-CN/docs/deployment
- 开发环境:config.dev.ts
- 生产环境:config.prod.ts
- 公共配置:config.ts 不带后缀
后端多环境实战
-
SpringBoot项目,通过application.yml添加不同的后缀来区分配置文件
- 主要是改依赖的环境地址和服务器配置:
- 数据库地址
- 缓存地址
- 消息队列地址
- 项目端口号
- 主要是改依赖的环境地址和服务器配置:
-
初始化数据库创建表的DDL要保存,在云端服务器执行查询语句即可
-
Maven的生命周期中执行package命令,进行打包
-
可在启动项目时添加启动变量
java -jar .\xxxx.jar --spring.profiles.active=prod
项目部署
参考文章:https://www.bilibili.com/read/cv16179200
需要Linux服务器(建议使用CentOS 8+/7.6+)
原始部署
什么都自己装
部署前端
需要web服务器:nginx、apache、tomcat
-
使用XSell或CRT连接服务器
-
安装配置nginx服务器: 参考文章:https://zhuanlan.zhihu.cpm/p/425790769
-
方式一:用系统自带的软件包管理器快速安装,比如CentOS的yum,Ubuntu的apt-get
-
方式二:自己到官网安装 下载稳定版本
复制下载链接
-
在服务器root目录下创建services目录
mkdir services
-
执行命令
curl -o nginx-1.22.1.tar.gz http://nginx.org/download/nginx-1.22.1.tar.gz
下载到服务器 -
执行命令
tar -zvxf nginx-1.22.1.tar.gz
解压 -
进入到
cd nginx-1.22.1/
执行configure./configure
-
安装https
yum install openssl openssl-devel -y
-
再次执行执行configure
./configure
-
设置系统配置参数
./configure --with-http_ssl_module --with-http_v2_module --with-stream
-
编译,执行
make
-
安装,
make install
-
配置环境变量
vim /etc/profile
在最后一行添加export PATH=$PATH:/usr/local/nginx/sbin
-
重启:
source /etc/profile
-
执行
netstat-ntlp
或netstat -tunpl
查看端口占用情况,可以发现nginx已经启动 -
修改
/usr/local/nginx/conf
下的配置文件(最好先不要修改原始配置文件):复制一份配置文件cp nginx.conf nginx.default.conf
-
前端项目build打包,压缩为dist.zip,上传到服务器,解压缩
unzip dist.zip
-
修改配置文件
vim nginx.conf
server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root /root/services/dist; index index.html index.htm; }
-
重新加载nginx配置
nginx -s reload
-
访问出现403错误
- 将nginx.config的user修改为和启动用户一致
- 执行
ps -ef|grep 'nginx'
发现nginx工作进程启动人为nobody - 修改nginx.config:
vim nginx.conf
的第一行为user root;
- 重新加载nginx配置
nginx -s reload
-
-
部署后端
java、maven
-
安装JDK:
yum install -y java-1.8.0-openjdk*
,输入java -version
验证java是否安装成功 -
安装Maven:
-
官网复制apache-maven-3.8.6-bin.tar.gz下载链接
-
执行
curl -o apache-maven-3.8.6-bin.tar.gz https://dlcdn.apache.org/maven/maven-3/3.8.6/binaries/apache-maven-3.8.6-bin.tar.gz
下载 -
执行命令
tar -zvxf apache-maven-3.8.6-bin.tar.gz
解压 -
进入
/root/service/apache-maven-3.8.6/bin
即可看到mvn -
配置环境变量
vim etc/profile
,按下shift+g定位到最后一行,按下i新增路径:export PATH=$PATH:/usr/local/nginx/sbin:/root/service/apache-maven-3.8.6/bin
-
esc+
:wq
保存退出 -
source /etc/profile
刷新配置 -
在root目录输入
mvn -v
即可验证是否安装成功
-
-
将后端代码提交到git(服务器需要有git)
-
在服务器克隆代码:
git clone 代码仓库地址
-
执行mvn编译打包构建:
mvn package-DskipTests
(-DskipTests跳过测试) -
构建完成后可以看到user-center-backend-0.0.1-SNAPSHOT.jar
-
改变权限:
chmod a+x user-center-backend-0.0.1-SNAPSHOT.jar
-
执行:
java -jar ./user-center-backend-0.0.1-SNAPSHOT.jar --spring.profile.active=prod
-
后台执行:
nohup java -jar ./user-center-backend-0.0.1-SNAPSHOT.jar --spring.profile.active=prod &
-
执行
jobs
命令即可看到当前运行的项目 -
执行
jps
命令可以查看所有已经运行的java程序 -
执行
netstat -ntlp
可以查看运行的端口和进程号
宝塔Linux部署
宝塔Linux是一个Linux运维面板,可参考官网安装脚本https://www.bt.cn/new/download.html
云服务器需要配置防火墙,允许本机ip访问
方便管理服务器、方便管理软件,软件商店安装Nginx、Tomcat9
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!