用户中心介绍
企业核心的用户中心系统,基于Spring Boot后端+React 前端的全栈项目,实现了用户注册、登录、查询等基础功能。
企业做项目流程
需求分析=>设计(概要设计、详细设计) => 技术选型 => 初始化/引入需要的技术 => 写Demo => 写代码(实现业务逻辑) => 测试(单元测试) => 代码提交/代码评审 = 部署 => 发布
需求分析
- 注册登录
- 用户管理(仅管理员可见)
- 对用户的查询或者修改
- 用户校验(仅星球用户)
技术选型
前端:
三件套+React+组件库 Ant Design+Umi+Ant Design Pro (现成的管理系统)
后端: java+spring +springmvc+mybatis+mybatis-plus+springboot+mysql
spring(依赖注入框架,帮助你管理Java 对象,集成一些其他的内容)
springmvc(web 框架,提供接口访问、restful接口等能力)
mybatis(Java 操作数据库的框架,持久层框架,对jdbc 的封装)
mybatis-plus(对 mybatis 的增强,不用写 sql 也能实现增删改查)
springboot(快速启动/快速集成项目。不用自己管理spring 配置,不用自己整合各种框架)
junit 单元测试库
部署:服务器/容器(平台)
项目开发流程
初始化项目
- 前端初始化
- 初始化项目
- 引入组件
- 框架代码/瘦身
- 后端初始化
- 环境准备
- 初始化后端项目,引入框架(整合框架)
- 前端初始化
数据库设计
登录注册
- 前端
- 后端
用户管理(仅管理员可见)
- 前端
- 后端
初始化项目
前端
初始化项目
npm i @ant-design/pro-cli@3.1.0 -g
pro create project
npm i yarn
yarn config set strict-ssl false
yarn add @umijs/preset-ui -D
框架代码/瘦身
1 | 2 | 3 |
---|---|---|
config (相关配置、路由等) | dist(部署的静态文件) | mock(模拟数据) |
public(静态文件) | public(静态文件) | public(静态文件) |
components(存放组件、页面是由多个组件构成的,组件可以复用) | pages(存放页面) | locales(国际化文件【语言】) |
e2e(测试流程) | services 下的 swagger 接口文档测试工具 | global.less(全局样式文件) |
global.tsx(全局脚本文件) | service-worker前端缓存 | eslintrc检查语法 |
prettierrc美化前端代码 |
后端
三种初始化java项目的方式
- GitHub搜现成的代码
- SpringBoot官方的模板生成器(https://start.spring.io/)
- 直接在IDEA开发工具中生成
如果要引l入java的包,可以去maven中心仓库寻找(http://mvnrepository.com/)
数据库设计
什么是数据库?存数据的
数据库里面有什么?数据表(理解为excel表格)
java操作数据库,代替人工操作excel
什么是设计数据库表?
有哪些表(模型)?表中有哪些字段?
性别是否需要加索引? 不需要
用户表
字段 | 内容 | 类型 |
---|---|---|
id | (主键) | bigint |
userAccount | ||
userName | 昵称 | varchar |
avatarUrl | 头像 | varchar |
gender | 性别 | tinyint |
userPassword | 密码 | varchar |
phone | 电话 | varchar |
邮箱 | varchar | |
userStatus | 用户状态 是否有效(是否被封号)[] | int |
createTime | 创建时间(数据插入时间) | datetime |
updateTime | 更新时间(数据更新时间) | datetime |
isDelete | 是否删除 0 1 (逻辑删除) | tinyint |
userRole | 用户角色0-普通用户1-管理员 | int |
CREATE TABLE `user`
(
`id` bigint AUTO_INCREMENT COMMENT 'id' PRIMARY KEY,
`username` varchar(256) DEFAULT NULL COMMENT '用户昵称',
`userAccount` varchar(256) DEFAULT NULL COMMENT '账号',
`avatarUrl` varchar(1024) DEFAULT NULL COMMENT '头像',
`gender` tinyint DEFAULT NULL COMMENT '性别',
`userPassword` varchar(512) NOT NULL COMMENT '密码',
`phone` varchar(128) DEFAULT NULL COMMENT '电话',
`email` varchar(512) DEFAULT NULL COMMENT '邮箱',
`userStatus` int DEFAULT 0 NOT NULL COMMENT '状态 0 - 正常',
`createTime` datetime DEFAULT CURRENT_TIMESTAMP NULL COMMENT '创建时间',
`updateTime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`isDelete` tinyint DEFAULT 0 NOT NULL COMMENT '是否删除'
) COMMENT ='用户';
登录注册
后端
规整目录
文件名 | 作用 |
---|---|
controller | 接收请求 |
mapper/dao | 数据访问 |
service | 业务逻辑 |
model | 用到的封装类 |
utils | 工具类(如日期转换) |
实现基本的数据库操作(user表)
- 模型user对象=>和数据库的字段关联,自动生成
自动生成器
MyBatisX插件,自动根据数据库生成domain实体对象、mapper(操作数据库的对象)、mapper.xml(定义了mapper对象和数据库的关联,可以在里面自己写SQL)service(包含常用的增删改查)、servicelmpl(具体实现service)
注册逻辑
账户在前端输入账户和密码、以及校验码(todo)
校验用户的账户、密码、校验密码,是否符合要求
- 非空
- 账户不小于4位
- 密码就不小于8位
- 账户不能重复
- 账户不包含特殊字符
- 密码与校验密码相同
对密码进行加密(密码千万不要直接以明文存储到数据库中)
向数据库插入用户数据
登录逻辑
登录功能(单机登录=>后续改为分布式/第三方登录)
接受参数:用户账户、密码
请求类型:POST
请求体: JSON格式的数据
请求参数很长时不建议用get
返回值: 用户信息(脱敏)
逻辑
校验用户账户和密码是否合法
- 非空
- 账户不小于4位
- 密码就不小于8位
- 账户不包含特殊字符
校验密码是否输入正确,要和数据库中的密文密码去对比
用户信息脱敏,隐藏敏感信息,防止数据库中的字段泄露
记录用户的登录态(session),将其存到服务器上(用后端SpringBoot框架封装的服务器tomcat去记录) cookie(如何知道是哪个用户登录了?)
返回脱敏后的信息
前端
删代码
- MFSU
- Ant Design Pro(Umi 框架)
- ProComponents高级表单
用户管理(仅管理员可见)
用户管理接口
!!! 必须鉴权
- 查询用户
- 允许根据用户名查询
- 删除用户
用户校验
仅适用于用户可信的情况
让用户自己填:填写编号/邀请码
后台补充对编号的校验:长度校验、唯一性校验 ==改为邀请码==
前端补充输入框,适配后端
后期拉取星球数据,定期清理数据==后期尝试增加邀请码==
优化
后端优化
通用返回对象
目的:给对象补充一些信息,告诉前端这个请求在业务层面上是成功还是失败
{ "name":"lihui" } ↓ // 成功 { "code":0//业务状态码 "data"{ "name":"lihui" }, "message":"ok" } // 错误 { "code":50001//业务状态码 "data":"null" "message":"用户操作异常" }
自定义错误码
返回类支持返回
封装全局异常处理
定义业务异常类
- 相对于java的异常类,支持更多字段
- 自定义构造函数,更灵活/快捷的设置字段
编写全局异常处理器 作用:
- 捕获代码中所有的异常,内部消化,让前端得到更详细的业务报错/信息
- 同时屏蔽掉项目框架本身的异常常(不暴露服务器内部状态)
- 集中处理,比如记录日志
实现
- SpringAOP:在调用方法前后进行额外的处理
全局请求日志和登录校验(//todo)
前端优化
- 对接后端的返回值,取data
- 全局响应处理
- 应用场景:我们需要对接口的通用响应进行统一处理,比如从response中取出data;或者根据code去集中处理错误,比如用户未登录、没权限之类的
- 优势:不用在每个接口请求中都去写相同的逻辑
- 实现:参考用的请求封装工具的官方文档,比如umi-resquest、axios等 参考各自的官方文档。创建新的文件,在该文件中配置一个全局请求类,在发送请求时,使用我们自己的定义的全局请求类。
部署
多环境
本地开发:localhost(127.0.0.1)
多环境:
指同一套项目代码在不同的阶段需要根据实际情况来部署到不同的机器上,并且部署到不同的机器上
为什么需要?
每个环境互不影响
区分不同的阶段:开发/测试/生产
对项目进行优化
- 本地日志级别
- 精简依赖,节省项目体积
- 项目的环境/参数可以调整 (比如JVM 参数)
针对不同环境做不同的事情
多环境分类
- 本地环境(自己的电脑)localhost
- 开发环境(远程开发)大家连同一台机器,为了大家开发方便
- 测试环境(测试)开发/测试/产品 【性能测试/功能测试/系统集成测试】独立数据库、独立服务器
- 预发布环境(体验服): 和正式环境一致,正式环境数据库,更严谨,查出更多问题
- 正式环境(线上,公开对外访问的项目)尽量不要改动,保证上线前的代码是"完美运行"
- 沙箱环境(实验环境):为了做实验
前端多环境实战
请求地址
开发环境:localhost:8080
线上环境:xxx.lihuibear.cn
jsstartFront(env){ if(env === 'prod'){ // 不输出注释 // 项目优化 // 修改请求地址 }else{ // 保持本地开发逻辑 } }
用了umi框架,build时会自动传入NODE_ENV==production 参数,start NODE_ENV参数为development
启动方式
- 开发环境:npmrunstart(本地启动,监听端口、自动更新)
- 线上环境:npm run build(项目构建打包),可以使用serve工具启动(npm i -g serve)
项目配置
不同的项目(框架)都有不同的配置文件,umi的配置文件是config,可以在配置文件后添加对应的环境名称后缀来区分开发环境和生产环境。参考文档:https://umijs.org/zh-CN/docs/deployment
- 开发环境:config.dev.ts
- 生产环境:config.prod.ts
- 公共配置:config.ts不带后缀
后端多环境实战
主要是Springboot项目,通过application.yml添加不同的后缀来区分配置文件
可以在启动项目时传入环境变量
java - jar xxx.jar --spring.profiles.active=prod
主要改
- 依赖的环境地址
- 数据库地址
- 缓存地址
- 消息队列地址
- 项目端口号
- 服务器配置
项目部署上线
需要Linux服务器(建议大家用CentOS8+/7.6以上)
- 原始前端、后端部署
什么都自己装
- 宝塔
宝塔面板
容器
docker是容器,可以将项目的环境(比如java、nginx)和项目的代码一起打包成镜像,所有同学都能下载镜像,更容易分发和移植。再启动项目时,不需要敲一大堆命令,而是直接下载镜像、启动镜像就可以了。docker可以理解为软件安装包。
Dockerfile
跨域问题解决
浏览器为了用户的安全,仅允许向同域名、同端口的服务器发送请求。
如果解决跨域
- 把域名、端口改成相同的
让服务器告诉浏览器:允许跨域(返回Access-Control-Allow-Origin 响应头)
网关支持(Nginx)
phplocation ^~ /api/ { proxy_pass:http://127.0.0.1:7070/api/; add_header 'Access-Control-Allow-Origin' $http_origin; add_header 'Access-Control-Allow-Credentials' 'true'; # 处理预检请求 if ($request_method = 'OPTIONS') { add_header 'Access-Control-Max-Age' 86400; add_header 'Access-Control-Allow-Origin' $http_origin; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'reqid, nid, host, x-requested-with, content-type'; add_header 'Content-Length' 0; add_header 'Content-Type' 'text/plain; charset=utf-8'; return 204; # 返回204 No Content } }
后端注解 @Crossorigin(origins =xxxxx)
前后端联调
// todo
用户中心项目扩展和规划(优化点)
- 功能扩展
- 管理员创建用户、修改用户信息、删除用户
- 上传头像
- 按照更多的条件去查询用户
- 更改权限
- 修改Bug
- 项目登录改为分布式session(单点登录 - redis)
- 通用性
- set-cookiedomain域名更通用,比如改为*.xxx.com
- 把用户管理系统=>用户中心(之后所有的服务都请求这个后端)
- 后台添加全局请求拦截器(统一去判断用户权限、统一记录请求日志)
补补知识
三种初始化java项目的方式
- GitHub搜现成的代码
- SpringBoot官方的模板生成器(https://start.spring.io/)
- 直接在IDEA开发工具中生成
如果要引l入java的包,可以去maven中心仓库寻找(http://mvnrepository.com/)
如何知道是哪个用户登录了?
- 连接服务器端后,得到一个session1状态(匿名会话),返回给前端
- 登录成功后,得到了登录成功的session,并且给该session设置一些值(比如用户信息),返回给前端一个设置cookie的"命令””
- session=>cookie
- 前端接收到后端的命令后,设置cookie,保存到浏览器内
- 前端再次请求后端的时候(相同的域名),在请求头中带上cookie去请求
- 后端拿到前端传来的cookie,找到对应的session
- 后端从session中可以取出基于该session存储的变量(用户的登录信息、登录名)
控制层Controller封装请求
@RestController适用于编写restful风格的api,返回值默认为json类型
controller层倾向于对请求参数本身的校验,不涉及业务逻辑本身(越少越好)
service层是对业务逻辑的校验(有可能被controller之外的类调用)
写代码流程
- 先做设计
- 代码实现
- 持续优化!!!(复用代码、提取公共逻辑/常量)
前后端交互
前端需要向后端发送请求
前端ajax 来请求后端
axios封装了ajax
request是antdesign项目又封装了一次
追踪request 源码:用到了umi的插件、requestConfig是一个配置
代理(解决跨域)
正向代理
替客户端向服务器发送请求,
反向代理
替服务器接收请求
怎么搞代理?
Nginx服务器、Nodejs服务器
MFSU
前端编译优化
Ant Design Pro(Umi 框架)
Ant Design组件库=>React
Ant Design Procomponents=>Ant Design
AntDesignPro后台管理系统=>AntDesign、React、Ant Design Procomponents、其他的库
app.tsx项目全局入口文件,定义了整个项目中使用的公共数据居(比如用户信息)
access.ts控制用户的访问权限
首次访问页面(刷新页面),进入app.tsx,执行getlnitialState方法,该方法的返回值就是全局可用的状态值。
ProComponents高级表单
- 通过columns定义表格有哪些列
- columns属性
- datalndex对应返回数据对象的属性
- title表格列名
- copyable是否允许复制
- ellipsis是否允许缩略
- valueType:用于声明这一列的类型(dateTime、select)
Dockerfile
Dockerfile用于指定构建Docker镜像的方法
Dockerfile一般情况下不需要完全从0自己写,建议去参考同类项目(比如springboot)
Dockerfile 编写
- FROM依赖的基础镜像
- WORKDIR工作目录
- COPY从本机复制文件
- RUN执行命令
- CMD/ENTRYPOINT(可以附加参数) 指定运行容器时默认执行的命令
构建Dockerfile镜像
# 后端
docker build -t user_center_back:v0.0.1 .
# 前端
docker build -t user_center_front:v0.0.1 .
Docker构建优化:减少尺寸、减少构建时间(如多阶段构建,可以丢弃之前阶段不需要的内容)
启动docker 镜像
docker run -p 7070:7070 -d user_center_back:v0.0.1
docker run -p 8080:80 -d user_center_front:v0.0.1
虚拟化
- 端口映射:把本机的资源和容器内部的资源进行关联
- 目录映射:把本机的端口和容器应用的端口进行关联
代码板子
特殊字符
//账户不能包含特殊字符(放在账户不能重复之前,省去一次数据库查询的资源浪费)
String validPattern = "\\W";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) { // 不要写反
return -1;
}
加密
MessageDigest md5 = MessageDigest.getInstance("MD5");
String newPassword = DigestUtils.md5DigestAsHex(("abcd" + "mypassword").getBytes());
System.out.println(newPassword);
注册
public long userRegister(String userAccount, String userPassword, String checkPassword) {
//1,校验
//非空
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
return -1;
}
//账户的话不小于4位,密码不小于8位
if (userAccount.length() < 4) {
return -1;
}
if (userPassword.length() < 8 || checkPassword.length() < 8) {
return -1;
}
//账户不能包含特殊字符(放在账户不能重复之前,省去一次数据库查询的资源浪费)
String validPattern = "[~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (!matcher.find()) {
return -1;
}
//账户不能重复
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
long count = userMapper.selectCount(queryWrapper);
if (count > 0) {
return -1;
}
//密码校验密码相同
if (!userPassword.equals(checkPassword)) {
return -1;
}
//2.加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
//3.插入数据
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 userLogin(String userAccount, String userPassword, HttpServletRequest request) {
//todo 修改自定义异常
// 1. 校验
// 非空
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
return null;
}
// 账户长度不小于4位,密码长度不小于8位
if (userAccount.length() < 4) {
return null;
}
if (userPassword.length() < 8) {
return null;
}
// 账户不能包含特殊字符
String validPattern = "\\W";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (matcher.find()) { // 不要写反
return null;
}
// 2.查询用户是否存在
// final String SALT = "lihui";
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;
}
//3.用户脱敏
// User safeUser = new User();
// safeUser.setId(user.getId());
// safeUser.setUsername(user.getUsername());
// safeUser.setUserAccount(user.getUserAccount());
// safeUser.setAvatarUrl(user.getAvatarUrl());
// safeUser.setGender(user.getGender());
// safeUser.setPhone(user.getPhone());
// safeUser.setEmail(user.getEmail());
// safeUser.setUserRole(user.getUserRole());
// safeUser.setUserStatus(user.getUserStatus());
// safeUser.setCreateTime(user.getCreateTime());
// safeUser.setIsDelete(0);
User safeUser = getSafetyUser(user);
//4.记录用户的登录态
request.getSession().setAttribute(USER_LOGIN_STATE, safeUser);
return safeUser;
}
用户脱敏
/**
* 用户脱敏
*
* @param originUser
* @return
*/
@Override
public User getSafetyUser(User originUser) {
User safeUser = new User();
safeUser.setId(originUser.getId());
safeUser.setUsername(originUser.getUsername());
safeUser.setUserAccount(originUser.getUserAccount());
safeUser.setAvatarUrl(originUser.getAvatarUrl());
safeUser.setGender(originUser.getGender());
safeUser.setPhone(originUser.getPhone());
safeUser.setEmail(originUser.getEmail());
safeUser.setUserRole(originUser.getUserRole());
safeUser.setUserStatus(originUser.getUserStatus());
safeUser.setCreateTime(originUser.getCreateTime());
safeUser.setIsDelete(0);
return safeUser;
}
Dockerfile
后端
# Docker 镜像构建
FROM maven:3.5-jdk-8-alpine as builder
# Copy local code to the container image.
WORKDIR /app
COPY pom.xml .
COPY src ./src
# Build a release artifact.
RUN mvn package -DskipTests
# Run the web service on container startup.
CMD ["java","-jar","/app/target/user-center-back-0.0.1-SNAPSHOT.jar","--spring.profiles.active=prod"]
前端
FROM nginx
WORKDIR /usr/share/nginx/html/
USER root
COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY ./dist /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.config
server {
listen 80;
# gzip config
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
root /usr/share/nginx/html;
include /etc/nginx/mime.types;
location / {
try_files $uri /index.html;
}
}
跨域
nginx网关
location ^~ /api/ {
proxy_pass:http://127.0.0.1:7070/api/;
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Credentials' 'true';
# 处理预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'reqid, nid, host, x-requested-with, content-type';
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain; charset=utf-8';
return 204; # 返回204 No Content
}
}