10764 字
54 分钟
用户中心项目学习

整个项目的学习目的:完整了解做项目的思路,接触一些企业级开发技术。

用户中心项目学习 01#

第一期目标:

  • 了解企业开发流程 ✅ 2026-01-07
  • 完成需求分析 ✅ 2025-12-22
  • 确认技术选型 ✅ 2025-12-22
  • 完成前端环境部署 ✅ 2025-12-23
  • 完成前端环境初始化(这里我直接复制了一份项目) ✅ 2025-12-23
  • 完成后端环境部署 ✅ 2025-12-23
  • 完成后端环境初始化 ✅ 2025-12-23
  • 使用docker多容器方式部署 ✅ 2025-12-23

企业开发项目流程#

需求分析 => 设计(概要设计、详细设计) => 技术选型 => 初始化/引入需要的技术 => 写Demo(测试引入的技术等) => 写代码(实现业务逻辑) => 测试(单元测试) => 代码提交/代码评审 => 部署 => 发布

需求分析#

了解为什么要做这个项目,这个项目需要完成什么功能,要解决什么问题。

用户中心#

这里的用户中心项目为的是可以提供给多个项目用户管理的功能。

  1. 登录 / 注册
  2. 用户管理(仅管理员可见)对用户查询或修改
  3. 用户校验(仅某些用户可以使用)

技术选型#

前端:三件套 + React + Ant Design 组件库 + Umi + Ant Design Pro(现成的管理系统)

后端:Java + Spring + SpringMVC + Mybatis + Mybatis-plus + SpringBoot + MySql + redis

部署:服务器 / 容器 (平台)

计划(逐步添加)#

  1. 初始化项目 (这里考虑使用docker)
    1. 前端初始化
      1. 初始化项目
      2. 引入组件
      3. 项目瘦身(现成的管理系统有些不需要用到)
    2. 后端初始化
      1. 准备环境
      2. 引入框架
  2. 数据库设计
  3. 登录\注册
    1. 前端
    2. 后端
  4. 用户管理(仅管理员可见)
    1. 前端
    2. 后端

前端初始化#

新版本的ant design pro#

使用pnpm: 根据官方文档安装pro-cli工具用来快速初始化脚手架 pnpm i @ant-design/pro-cli -g

接着使用 pro create myapp 来创建项目

创建后进入项目目录 使用pnpm install 安装依赖 使用 pnpm start 启动项目

image.png

由于官方新版本的ant design pro使用的是umi4 而umi4不支持uni ui(直接ui界面导入模块和模板页面)

因此我需要想办法使用回老版本的ant design pro

老版本的ant design pro#

这里直接指定版本安装和教学视频同版本的ant design pro

步骤:

  1. 先安装老版本的脚手架(3.1.0) pnpm i @ant-design/pro-cli@3.1.0 -g
  2. 再使用pro create myapp来创建项目 就会包含umi@3选项
  3. 选择umi@3项目完成创建

但是这里还有一个问题,就是umi@3要求node版本在16以及16以下 因此这里书写dockerfile来解决环境问题

思考过程: 无论前端还是后端,写 Dockerfile 永远遵循这 4 个步骤:

  1. 底座 (FROM):我要用什么系统?(比如:我要有 Java 环境,或者要有 Node 环境)。

  2. 搬运 (COPY):把我家里的东西(代码/包)搬到这台新电脑里。

  3. 安装 (RUN):在新电脑里执行什么安装命令?(比如 npm install)。

  4. 启动 (CMD/ENTRYPOINT):电脑开机后,自动运行什么程序?

书写dockerfile:

# 1. 底座:明确指定 Node 16,锁死环境
FROM node:16
# 2. 建立工作目录
WORKDIR /app
# 强制安装 pnpm 的 8.x 版本,因为它才支持 Node 16
RUN npm install -g pnpm@8
# --- 技巧开始 ---
# 3.1 先只搬运依赖描述文件
COPY package.json ./
# 3.2 安装依赖 (RUN 是在构建镜像时执行的命令)
# 只要 这两个文件 没变,这层缓存就会被利用,速度飞快
# 一般会使用 pnpm install # --frozen-lockfile 相当于 npm ci,表示:严格按照 lock 文件装, # 如果 lock 文件和 package.json 对不上,就报错(防止偷偷升级版本)
# 但是这里因为我本地电脑是新版本的pnpm 内部环境和新本的格式不同 会导致报错 这里直接不传入pnpm-lock.yaml 也不强制要求 就可以解决报错问题
RUN pnpm install
# --- 技巧结束 ---
# 3.3 依赖装好了,再把剩下的所有代码搬进去
COPY . .
# 4. 暴露端口 (给看代码的人看的,实际要靠 docker-compose 映射)
EXPOSE 3000
# 5. 启动:容器跑起来后执行的命令
CMD ["pnpm", "start"]

书写.dockerignore文件 用来忽略不需要转到环境内的文件

# 必须要忽略的巨无霸
node_modules
.git
# 忽略构建产物(如果有)
dist
build
.idea
.vscode

在当前目录下执行指令 docker build -t user-center . 进行构建 构建后 使用docker run -p 3000:3000 my-frontend-app 运行镜像并且映射3000端口

项目结构#

学习视频里使用的前端React较老 目前主流的路由方式和视频中不同 (比如新版还有快照) 之后再重新学习一下React

项目瘦身#

这里删掉了i18n 多国语言支持 e2e (集成测试) 删除一些不需要的页面(需要把路由一起删了,但是新版React的路由是根据文件目录的) 删掉Swagger 删掉openapi文件(删掉后要把依赖的代码去掉)

[!BUG] 注意 由于版本偏老,最终还是出现了不同的问题 比如齿轮并没有显示,并且umi ui也没有出现 最后决定先不考虑前端项目实现,直接使用现成的前端项目,同样的使用docker配置后直接运行。

后端初始化#

docker-compose配置#

version: '3.8' # 版本号,一般用3.8即可
services:
# --- 角色1: MySQL ---
mysqldb: # 服务名,这就是内部的域名!Java连接时host填这个
image: mysql:8.0
container_name: my_project_mysql
environment:
MYSQL_ROOT_PASSWORD: 123456 # 环境变量
MYSQL_DATABASE: my_app_db # 自动初始化的库名
ports:
- "3306:3306" # 左边是宿主机端口(你电脑的),右边是容器内部端口
volumes:
- ./mysql-data:/var/lib/mysql # 挂载数据,防止丢数据
networks:
- my-net # 加入统一的网络
# --- 角色2: Redis ---
redis:
image: redis:latest
container_name: my_project_redis
ports:
- "6379:6379"
networks:
- my-net
# --- 角色3: SpringBoot 后端 --- 这里在开发完成之前先不需要创建后端容器
backend:
build: ./backend # 告诉docker去 './backend' 目录找 Dockerfile 编译
container_name: my_project_backend
ports:
- "8080:8080"
depends_on: # 只有这两个起好了,我再起
- mysqldb
- redis
environment:
# 这里的关键点:数据库地址写的是 'mysqldb',不是 localhost
SPRING_DATASOURCE_URL: jdbc:mysql://mysqldb:3306/my_app_db?useSSL=false
SPRING_REDIS_HOST: redis
networks:
- my-net
# --- 角色4: Node 前端 (开发模式) ---
frontend:
build: ./frontend # 指定前端dockerfile路径
container_name: my_project_frontend
working_dir: /app
volumes:
- ./frontend:/app # 把代码挂载进去,方便改代码实时生效
- /app/node_modules # 这是一个技巧,防止容器内外的 node_modules 冲突
ports:
- "3000:8000"
command: pnpm start # 覆盖默认命令,启动开发服务器
networks:
- my-net
# 定义一个网络,让大家在同一个局域网里
networks:
my-net:
driver: bridge

首先创建了springboot项目#

使用idea创建了 2.7.4版本的Springboot项目 添加了基础的mybatis lombok spring web等依赖

添加mybatis-plus依赖并书写简单demo进行测试#

根据官方文档,添加依赖并且写官方示例代码(写项目时最好不要直接开始写业务代码,应该要先写一些demo来确认可行性防止出各种问题)

初始化项目#

这里同样为了版本一致性 直接下载了写完之后的项目删掉了全部业务代码 只保留基本的框架内容

注意: 由于使用了docker-compose来整合整个项目,application.yml配置文件需要注意一件事情,在项目目前开发阶段可以写localhost来连接数据源,但是当要把整个后端作为容器运行时,需要改为服务名不能使用localhost,可以到时候写第二份配置文件来解决这个问题。

也可以直接docker-compose中改配置:

services:
backend:
# ... 其他配置 ...
environment:
# 在这里覆盖掉配置文件里的 localhost
# 注意:Spring Boot 会自动把这些大写变量映射到配置项
SPRING_DATASOURCE_URL: jdbc:mysql://mysqldb:3306/my_app_db?useSSL=false
SPRING_REDIS_HOST: redis

用户中心项目学习 02#

目标:

  • 完成用户库表设计 ✅ 2025-12-24
  • 完成注册的后端开发 ✅ 2025-12-24
  • 完成登录的后端开发 ✅ 2025-12-29
  • 完成后端用户的管理接口 ✅ 2025-12-30

数据库设计#

设计数据库表需要思考

  • 有哪些表(模型)
  • 表中有哪些字段?
  • 字段的类型?
  • 数据库字段添加索引?想想查询时经常用到什么字段去进行查询
  • 表与表之间的关系?

用户表: id(主键) varchar username 名称 varchar userAccount 登录账号 varchar avatarUrl 头像 varchar gender 性别 tinyint userPassword 密码 varchar phone 电话 varchar email 邮箱 varchar userStatus 用户状态 int 0-正常 1-封禁 createTime 创建时间 datetime updateTime 更新时间 datetime isDelete 是否删除(逻辑删除)tinyint

书写sql代码

create table user
(
username varchar(256) null comment '用户昵称',
id bigint auto_increment comment 'id'
primary key,
userAccount varchar(256) null comment '账号',
avatarUrl varchar(1024) null comment '用户头像',
gender tinyint null comment '性别',
userPassword varchar(512) not null comment '密码',
phone varchar(128) null comment '电话',
email varchar(512) null comment '邮箱',
userStatus int default 0 not null comment '状态 0 - 正常',
createTime datetime default CURRENT_TIMESTAMP null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
isDelete tinyint default 0 not null comment '是否删除',
)
comment '用户';

完成登录注册功能#

一般建议先写后端

后端#

1.创建基本的目录结构#

controller
mapper
model
service
utils

2.实现基本数据库操作(操作user表)#

模型user对象 => 和数据库关联(使用自动生成的方式)

自动生成模型(使用mybatisX插件)#

右键表 点击MybatisX-generator 选择模块路径 image.png 如图选择 image.png

即可生成代码

生成后手动移动到目录内

添加测试类测试userService能不能正常使用#

这里生成对象所有的set方法或get方法可以使用插件:GenerateAllSetter

然后使用快捷操作(alt+enter) 选择生成 即可一键填充全部字段 image.png

ctrl+p 可以查看函数有哪些参数

/**
* 用于测试UserService类 * * @author ShineAcZ
* @date 2025/12/24
*/@SpringBootTest
class UserServiceTest {
@Resource
UserService userService;
@Test
void testAddUser(){
User user = new User();
user.setUsername("testUser");
user.setUserAccount("123");
user.setAvatarUrl("https://tuchuang-1353351309.cos.ap-guangzhou.myqcloud.com/picture/20250904232225476.png");
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);
}
}

写完后测试 记得添加MapperScan注解

/**
* 启动类 * */
@SpringBootApplication
// 要记得添加这个注解来扫描mapper里的全部类
@MapperScan("top.shineacz.usercenter.mapper")
public class UserCenterApplication {
public static void main(String[] args) {
SpringApplication.run(UserCenterApplication.class, args);
}
}

实现注册功能#

首先思考注册的逻辑:#
  1. 用户在前端输入账号和密码、以及验证码(todo)
  2. 校验用户的账户、密码、校验密码是否符合要求
    1. 账号不小于4位
    2. 密码不小于八位
    3. 账号不能重复
    4. 账号不能包含特殊字符
    5. 密码和校验密码相同
  3. 对密码进行加密
  4. 插入数据到数据库
具体代码实现:#
public long userRegister(String userAccount, String userPassword, String checkPassword) {
// 1.校验
// 是否有任意一个为空(使用了commons.lang3库) 也可以使用Hutools的StrUtils.hasBlank if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
return -1;
}
// 账号是否小于4位
if (userAccount.length() < 4) {
return -1;
}
// 密码是否小于8位
if (userPassword.length() < 8 || checkPassword.length() < 8) {
return -1;
}
// 密码和校验密码不相同
if(!userPassword.equals(checkPassword)){
return -1;
}
// 账号不能包含特殊字符 只允许字母数字下划线
String regex = "^[a-zA-Z0-9_]+$";
if (!Pattern.matches(regex, userAccount)) {
return -1;
}
// 检查数据库中是否重复 这里使用 count方法
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
long count = userMapper.selectCount(queryWrapper);
if (count > 0) {
return -1;
}
// 2. 对密码进行加密
final String SALT = "shine";
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
// 3. 插入数据
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
// 影响的行数
int result = userMapper.insert(user);
if(result == 0) {
return -1;
}
return user.getId();
}

写完逻辑后要写单元测试进行功能的测试(这里只简单测试了一下 可以写完整的全路径测试)

@Test
void userRegister() {
String username = "testName";
String password = "123456789";
String checkPassword = "123456789";
long result = userService.userRegister(username, password, checkPassword);
System.out.println(result);
assertTrue(result > 0);
}

实现登录功能#

考虑请求#

接受参数:账号、密码 请求类型:POST 请求体:Json格式 返回值:用户信息 (脱敏)

首先思考登录的逻辑:#
  1. 校验用户账号密码是否合法
    1. 非空
    2. 账户长度不小于4位
    3. 密码就不小于8位吧
    4. 账户不包含特殊字符
  2. 校验密码是否输入正确,要和数据库中的密文密码去对比
  3. 记录用户登录态(session),存到服务器上(用tomcat去记录)
  4. 返回用户信息(脱敏)
如何知道是哪个用户登录了?#
  1. 连接服务器端后,得到一个session状态,返回给前端
  2. 登录成功后,得到了登录成功的session,并且给该session设置一些值(比如用户信息),返回给前端一个设置cookie的”命令”
  3. 前端接收到后端的命令后,设置cookie,保存到浏览器内
  4. 前端再次请求后端的时候(相同的域名),在请求头中带上cookie去请求
  5. 后端拿到前端传来的cookie,找到对应的session
  6. 后端从session中取出用户的登录信息、登录名
具体代码实现:#
/**
* 用户服务实现类 * * @author shine
* @description 针对表【user(用户)】的数据库操作Service实现
* @createDate 2025-12-24 00:27:35
*/@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Resource
UserMapper userMapper;
/**
* 盐值,用于混淆密码 */ private static final String SALT = "shine";
/**
* 用户登录态键 */ private static final String USER_LOGIN_STATE = "userLoginState";
@Override
public long userRegister(String userAccount, String userPassword, String checkPassword) {
// 1.校验
// 是否有任意一个为空(使用了commons.lang3库) 也可以使用Hutools的StrUtils.hasBlank if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
return -1;
}
// 账号是否小于4位
if (userAccount.length() < 4) {
return -1;
}
// 密码是否小于8位
if (userPassword.length() < 8 || checkPassword.length() < 8) {
return -1;
}
// 密码和校验密码不相同
if(!userPassword.equals(checkPassword)){
return -1;
}
// 账号不能包含特殊字符 只允许字母数字下划线
String regex = "^[a-zA-Z0-9_]+$";
if (!Pattern.matches(regex, userAccount)) {
return -1;
}
// 检查数据库中是否重复 这里使用 count方法
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
long count = userMapper.selectCount(queryWrapper);
if (count > 0) {
return -1;
}
// 2. 对密码进行加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
// 3. 插入数据
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(userPassword);
int result = userMapper.insert(user);
if(result == 0) {
return -1;
}
return user.getId();
}
@Override
public User doLogin(String userAccount, String userPassword, HttpServletRequest request) {
// 1.校验
// 是否有任意一个为空(使用了commons.lang3库) 也可以使用Hutools的StrUtils.hasBlank if (StringUtils.isAnyBlank(userAccount, userPassword)) {
return null;
}
// 账号是否小于4位
if (userAccount.length() < 4) {
return null;
}
// 密码是否小于8位
if (userPassword.length() < 8) {
return null;
}
// 账号不能包含特殊字符 只允许字母数字下划线
String regex = "^[a-zA-Z0-9_]+$";
if (!Pattern.matches(regex, userAccount)) {
return null;
}
// 2.加密
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 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. 记录用户登录态 存一份session 内容为user对象
request.getSession().setAttribute(USER_LOGIN_STATE,safetyUser);
return safetyUser;
}
}

注意: 查询时得考虑逻辑删除的问题,如果删除不能给查询,但是MyBatis-Plus可以配置自动逻辑删除,这里配置一下就好了。 步骤一:

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

步骤二: 加上逻辑删除注解: image.png

这里代码中暂时没有写自定义异常,只返回了-1,之后需要修改。(TODO)

实现接口层#

@RestController 适用于编写restful风格api,返回值默认为json类型。

@RestBody 用于让controller能自动将参数转化为对象

可以使用auto fill填充插件,快速填充函数参数。

controller层倾向于对请求参数本身的校验,不涉及业务逻辑本身 (越少越好) service层是对业务逻辑的校验(有可能被controller之外的类调用)

书写登录注册接口:

package top.shineacz.usercenter.controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.shineacz.usercenter.model.domain.User;
import top.shineacz.usercenter.model.domain.request.UserLoginRequest;
import top.shineacz.usercenter.model.domain.request.UserRegisterRequest;
import top.shineacz.usercenter.service.UserService;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* 用户接口层 * * @author ShineAcZ
* @date 2025/12/28
*/@RestController
@RequestMapping("/user")
public class UserController {
@Resource
UserService userService;
@PostMapping("/register")
public long userRegister(@RequestBody UserRegisterRequest userRegisterRequest){
if(userRegisterRequest == null) return -1;
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if(StringUtils.isAnyBlank(userAccount,userPassword,checkPassword))return -1;
return userService.userRegister(userAccount, userPassword, checkPassword);
}
@PostMapping("/login")
public User userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request){
if(userLoginRequest == null) return null;
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserPassword();
if(StringUtils.isAnyBlank(userAccount,userPassword)) return null;
return userService.userLogin(userAccount, userPassword, request);
}
}
测试:#

Idea自带测试工具,点击controller左边的小图标,可以快速创建。

用户管理接口#

!!!必须鉴权

这里为了鉴权,需要添加一个字段:userRole 值为0和1 0表示普通用户 1代表管理员

添加后需要修改那些被影响到的地方。

需要加上Session过期时间

spring:
# session 失效时间 24小时
session:
timeout: 86400
写代码时的思想:#
  1. 先做设计
  2. 代码实现
  3. 持续优化(复用代码、提取公共逻辑)
功能:#
  1. 查询用户
    1. 允许根据用户名查询
  2. 删除用户

查询用户代码:

// -- controller
@GetMapping("/search")
public List<User> searchUser(String username, HttpServletRequest request) {
if (!isAdmin(request)){
return new ArrayList<>();
}
// 查询用户->调用service查找
return userService.searchUserByUsername(username);
}
// -- service
@Override
public List<User> searchUserByUsername(String username) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
queryWrapper.like("username", username);
}
List<User> userList = userMapper.selectList(queryWrapper);
// 脱敏
return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
}
//提取脱敏方法
@Override
public User getSafetyUser(User user) {
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.setUserRole(user.getUserRole());
safetyUser.setUserStatus(user.getUserStatus());
safetyUser.setCreateTime(user.getCreateTime());
return safetyUser;
}

前端#

为了能够学习前端的内容,重新配置了一下环境,在本地电脑进行开发。 使用nvm快速切换node版本到16

安装pnpm 的 8.x 版本 npm install -g pnpm@8

安装必要依赖 pnpm add @ant-design/pro-layout@^6.38.0 pnpm install pnpm add umi-request

运行 pnpm start (不使用mock启动)

步骤:

  1. 修改登录页面各种信息
  2. 删除多余代码

在app.ts中写入RequestConfig 可以配置请求地址前缀

前后端交互#

前端需要向后端发送请求 前端使用ajax来请求后端 一般会用axios axios封装了ajax比较好用 而request 是 ant design 项目又进行了一次封装 这里追踪request 源码:发现项目请求用到了 umi 的插件、其中能得知requestConfig 是一个配置 最终就可知道配置的方式。

代理#

正向代理:替客户端向服务器发送请求 反向代理:替服务器接受请求并分给服务器(可能有多台服务器)如下图: image.png

怎么搞代理? 一般可以用下面两种方式,总之是靠服务 使用Nginx服务器 使用Node.js服务器

而在这个前端项目中,可以直接在config文件夹中的proxy.ts进行代理配置(使用node.js)

后端可以配置全部请求地址前缀,如下:

serve:
port: 8080
servlet:
context-path: /api

前端配置代理后,因为要请求到自己的地址才会被转发,所以前面关于前缀的配置得删掉。(绕了一圈属于是)

用户中心项目学习 03#

目标:

  • 完成登录的前端开发 ✅ 2026-01-02
  • 完成注册的前端开发 ✅ 2026-01-02
  • 完成用户管理后台的前端开发 ✅ 2026-01-02
  • 完成前端代码瘦身和优化 ✅ 2026-01-07
  • 完成后端代码优化 ✅ 2026-01-07
  • 思考新需求-开发星球用户校验功能 ✅ 2026-01-07
  • 项目扩展思路 ✅ 2026-01-07

首先完成了前端的注册页面,基本上是复制登录页面然后加点东西该点属性完成的。

接着完成前端注册逻辑: 校验两次密码一致 校验符合一定规则 新增一些方法用于请求 修改请求参数符合注册需要的参数

前端开发的登录拦截: 一般会添加一个redirect参数在网址来表示之前的页面是什么,然后根据这个值在登录成功后跳转回去。

待优化点:#

  1. 注册失败没有友好提示
  2. 前端密码重复提示可以优化

后端获取用户信息接口#

从session中获取即可

书写接口current来获取当前登录用户的数据。 这里要注意,因为session中存的数据并不会实时跟着改变,为了防止数据变化导致的问题,这里不直接返回session中存的对象, 而是重新根据id获取数据再返回。

代码:

@GetMapping("/current")
public User getCurrentUser(HttpServletRequest request) {
Object objUser = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
User currentUser = (User) objUser;
if(currentUser == null) return null;
// TODO 校验用户是否被封号
// 若存在则去数据库中读取最新的用户数据并返回
User user = userService.getById(currentUser.getId());
// 脱敏
return userService.getSafetyUser(user);
}

并且顺便修改了一下getSafetyUser的代码: 添加了user为空的校验

前端书写管理页面#

前端管理页面的书写暂时跳过,基本上React和基本的前后端知识,通过修改组件代码和内容实现。

后端注销接口#

思路就是清除存储的session

代码:

//-- controller
@PostMapping("/logout")
public Integer userLogout(HttpServletRequest request) {
if (request == null) {
return 0;
}
return userService.userLogout(request);
}
//-- service
@Override
public int userLogout(HttpServletRequest request) {
request.getSession().removeAttribute(USER_LOGIN_STATE);
return 1;
}

添加星球用户校验功能#

  • 这里使用比较简单的实现方式,让用户自己填写星球编号 然后到时候服务器想办法拿到编号进行匹配是否符合。
  1. 首先给表添加新的字段用于填写星球编号
  2. 为注册方法添加星球编号参数,并且判断是否合法以及检查数据库中是否重复
  3. 修改被影响到的地方
  4. 在前端中添加一个输入框,用于获取星球编号。

代码优化#

1. 后端通用返回对象#

目的:给对象补充一些信息,告诉前端这个请求中业务层面上是成功还是失败。

{
"name":"shine"
}
// 成功
{
"code": 200,//业务状态码
"data": {
"name":"shine"
},
"message":"ok"
}
// 失败
{
"code": 50001,//业务状态码
"data": null,
"message":"注册xxx异常xxxx"
}

创建BaseResponse对象,封装返回的东西

@Data
public class BaseResponse<T> implements Serializable {
private static final long serialVersionUID = -8220981337723599912L;
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, null);
}
}

创建ResultUtils对象 封装常用的情况

/**
* 返回结果工具类 * * @author ShineAcZ
* @date 2026/1/2
*/public class ResultUtils {
public static <T> BaseResponse<T> success(T data){
return new BaseResponse<>(0, data, "ok");
}
}

2. 后端封装全局异常处理#

定义ErrorCode枚举类 添加基本的参数 message是大体的错误 description是具体错误的描述 写上自定义的错误码

/**
* 错误码枚举类 * * @author ShineAcZ
* @date 2026/1/2
*/@Getter
@AllArgsConstructor
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;
}
/**
* 响应基类 * * @author ShineAcZ
* @date 2026/1/2
*/@Data
public class BaseResponse<T> implements Serializable {
private static final long serialVersionUID = -8220981337723599912L;
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) {
this(code, data, "","");
}
public BaseResponse(int code, T data, String message) {
this(code, data, message,"");
}
public BaseResponse(ErrorCode errorCode){
this(errorCode.getCode(),null, errorCode.getMessage());
}
}
/**
* 返回结果工具类 * * @author ShineAcZ
* @date 2026/1/2
*/public class ResultUtils {
/**
* 返回成功 * @param data 返回的数据
* @return 返回响应对象
* @param <T> 数据类型
*/ public static <T> BaseResponse<T> success(T data){
return new BaseResponse<>(0, data, "ok");
}
/**
* 返回失败 * @param errorCode 错误码和信息对象
* @return 返回响应对象
* @param <T> 数据类型
*/ public static <T> BaseResponse<T> error(ErrorCode errorCode){
return new BaseResponse<>(errorCode);
}
}
封装全局异常处理:#

目的是为了直接能处理代码里的异常,然后自己用BaseResponse封装后返回

  1. 先定义全局异常
    1. 相对于Java的异常类,能支持更多字段
    2. 自定义构造函数,使用更灵活
/**
* 自定义全局业务异常 * * @author ShineAcZ
* @date 2026/1/5
*/@Getter
public class BusinessException extends RuntimeException {
private int code;
private 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;
}
}

将出问题时return的地方改为抛出异常(传入descrition参数)

if (userRegisterRequest == null) {
// return ResultUtils.error(ErrorCode.PARAMS_ERROR);
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 1.校验
// 是否有任意一个为空(使用了commons.lang3库) 也可以使用Hutools的StrUtils.hasBlank
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"参数为空");
}
// 账号是否小于4位
if (userAccount.length() < 4) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账号不能小于4位");
}
// 密码是否小于8位
if (userPassword.length() < 8 || checkPassword.length() < 8) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码不能小于8位");
}
// 密码和校验密码不相同
if (!userPassword.equals(checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码和校验密码不相同");
}
// 星球编号不大于5位
if (planetCode.length() > 5){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"星球编号不能大于5位");
}
// 账号不能包含特殊字符 只允许字母数字下划线
String regex = "^[a-zA-Z0-9_]+$";
if (!Pattern.matches(regex, userAccount)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账号不能包含特殊字符");
}
  1. 编写全局异常处理器
    1. 捕获代码中所有异常,内部消化、集中处理,让前端得到更详细的业务报错/信息
    2. 屏蔽掉项目框架本省的异常不让内部暴露。 实现:使用 @RestControllerAdvice 注解 和@ExceptionHandler(BusinessException.class) (代表捕获括号内这个异常)

记得打错误日志

/**
* 全局异常处理类 * * @author ShineAcZ
* @date 2026/1/5
*/@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public <T> BaseResponse<T> businessExceptionHandler(BusinessException e){
log.error("BusinessException:" + e.getMessage(), e);
return ResultUtils.error(e.getCode(), e.getMessage(), e.getDescription());
}
@ExceptionHandler(RuntimeException.class)
public <T> BaseResponse<T> RuntimeExceptionHandler(RuntimeException e){
log.error("RuntimeException", e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "");
}
}

3. TODO 后端全局请求日志和登录校验#

4. 前端优化#

  1. 定义通用返回对象,对接后端的返回值
  /**
   * 通用返回类
   */
  type BaseResponse<T> = {
    code: number,
    data: T,
    message: string,
    description: string,
  }
  1. 修改代码显示信息
  2. 修改响应拦截器,使其在code等于1时,自动获取data 这里需要自己再定义reque st对象
    1. 应用场景:我们需要对接口的通用响应进行统一处理,比如从response中取出data;或者根据code去集中处理错误,比如用户未登录、没权限之类的。
    2. 优势:不用在每个接口请求中都去写相同的逻辑
    3. 实现:参考你用的请求封装工具的官方文档,如果你用axios,参考axios的文档。创建新的文件,在该文件中配置一个全局请求类。在发送请求时,使用我们自己的定义的全局请求类。

用户中心项目学习 04#

  • 项目部署服务器 ✅ 2026-01-07

多环境#

多环境参考文章:多环境设计

多环境:指同一套项目代码这不同的阶段需要根据实际情况来调整配置并部署到不同的机器上。

为什么需要多环境?

  1. 每个环境互不影响
  2. 为了区分不同的阶段:开发、测试、生产
  3. 对项目进行优化:
    1. 本地日志级别
    2. 精简依赖,节省项目体积
    3. 项目的环境 / 参数可以调整,比如JVM参数 针对不同环境做不同的事情。

多环境分类:

  1. 本地环境(自己的电脑)localhost
  2. 开发环境(远程开发)大家连同一台环境,为了大家开发方便。
  3. 测试环境(测试,单元测试 / 性能测试 / 功能测试 / 双系统集成测试)开发 / 测试 / 产品
  4. 预发布环境(体验服)基本和正式环境一致,正式数据库,更严谨,查出更多问题。
  5. 正式环境(线上,公共对外访问的项目)尽量不要改动,保证上线前的代码是”完美”运行
  6. 沙箱环境(实验环境)为了做实验

前端多环境实战#

  • 请求地址
    • 开发环境:localhost:8080
    • 线上环境: (user-backend.code-nav.cn) 注册的域名地址

通过代码控制不同环境的情况

startFront(env){
if(env === 'prod'){
// 不输出注释
// 项目优化
// 修改请求地址
} else {
// 保持本地开发逻辑
}
}

用了umi框架:

build时会自动传入NODE_ENV==production 参数 start NODE_ENV参数为development

启动方式:

  • 开发环境:npm run start(本地启动,监听端口、 自动更新)
  • 线上环境:npm run build (项目构建打包),可以使用serve工具启动(直接在dist中输入serve命令 需要先 npm install serve)

配置不同环境下的请求地址:

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

项目的配置: 不同的项目(框架)都有不同的配置文件,umi的配置文件是config

  • 可以在配置文件后添加对应的环境名称后缀来区分开发环境和生产环境o开发环境:config.dev.ts
  • 生产环境:config.prod.ts

前端静态化知识:#

静态化,就是将一个原本需要“实时计算生成”的动态网页,提前生成为一个“固定不变”的HTML文件。

如果不开启静态化,会被编译成只有一个index主页面,如果开启静态化将会编译出多个页面 “一次动态生成,多次静态服务”

  1. 发布/触发时:当编辑发布一篇新博客,或者商品信息更新时,系统自动调用一次后台的动态生成流程(查询数据库、渲染模板),但生成的结果不是一个瞬间消失的页面,而是被保存为一个实实在在的 .html 文件,存放在服务器的硬盘上。
  2. 用户访问时:当任何用户来访问这个页面时,服务器不再执行复杂的数据库查询和程序逻辑,而是直接把这个预先生成好的 .html 文件发送给用户,就像访问一个纯静态页面一样。

后端多环境实战#

SpringBoot项目:

  1. 首先通过application.yml / application-prod.yml 来区别不同的环境。 如果使用docker 就写服务名: MySQL 配置:
  • 原来(本地跑): jdbc:mysql://localhost:3306/my_app_db
  • 现在(Docker跑): jdbc:mysql://mysqldb:3306/my_app_db

image.png

  1. 对项目进行打包:使用Maven生命周期的package 这里在打包的时候如果测试不过会报错,正常应该修改测试,这里先跳过测试模式: image.png

  2. 打包后会在taget文件夹内生产一个jar文件

  3. 通过运行jar这个jar文件传入参数控制使用的配置文件(建议用生产环境的打包,然后运行时再用参数去选择其他环境配置)

Terminal window
java -jar .\user-center-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

项目部署#

原始 Nginx + Spring Boot#

前端#

需要web服务器:Nginx、apache、tomcat

安装Nginx服务器:

  1. 使用系统软件包管理器安装,比如apt、yum
  2. 下载官方包手动配置和make编译 配置环境变量

使用方法1:

Terminal window
1.//下载nginx 所需要的依赖包
yum -y install gcc pcre-devel zlib-devel openssl openssl-devel
2.http://nginx.org/ //从官网下载选择安装包
3.wget http://nginx.org/download/nginx-1.19.7.tar.gz
//然后将下载的文件 解压
tar -xf nginx-1.19.7.tar.gz
4. //进入 nginx-1.19.7
cd nginx-1.19.7
//执行下面代码
./configure --prefix=/usr/local/nginx
5.//然后执行
make&&make install
6. //cd到刚才配置的安装目录/usr/local/nginx/sbin
./nginx
7. 在浏览器输入服务器的ip地址 会显示 Welcome to nginx! 就安装完成了
8.//软连接
ln -s /usr/local/nginx/sbin/nginx /usr/local/bin
就可以全局使用 nginx 来启动了
nginx //启动
nginx -s stop //关闭 nginx
nginx -s reload //重启nginx

查看当前网络连接和监听端口 netstat -ntlp

Nginx常用命令:

Terminal window
# 测试配置文件语法是否正确
nginx -t
# 重新加载配置文件(不重启服务)
nginx -s reload
# 查看Nginx版本和编译参数
nginx -V
# 启动/停止/重启Nginx服务
systemctl start|stop|restart nginx # 系统使用systemd
service nginx start|stop|restart # 旧系统使用init

Nginx配置文件:

# ==================== 主配置上下文(main context)====================
# 这一部分配置在文件最外层,对整个Nginx服务生效
# 定义运行Nginx工作进程的用户和用户组
# 默认使用"nginx"或"www-data",实际应根据系统用户设置
user nginx;
# 定义工作进程的数量。通常设置为CPU核心数或auto(自动检测)
# 高并发场景可以设置为CPU核心数的倍数(如 worker_processes 8;)
worker_processes auto;
# 定义错误日志文件的位置和日志级别
# 日志级别:debug, info, notice, warn, error, crit(严重性递增)
# 生产环境通常用 error 或 warn
error_log /var/log/nginx/error.log notice;
# 定义存储Nginx主进程ID的文件位置
pid /var/nginx/nginx.pid;
# ==================== 事件模块配置(events context)====================
# 配置影响Nginx服务器与用户的网络连接
events {
# 每个worker进程能同时处理的最大连接数(包括与客户端的连接和后端服务器的连接)
# 这个值直接影响Nginx的并发能力。理论最大连接数 = worker_processes * worker_connections
worker_connections 1024;
# 使用epoll高效I/O模型(Linux系统推荐)
# 在FreeBSD系统上则使用kqueue,Windows没有
use epoll;
# 开启多连接接受模式,允许worker进程同时接受多个新连接
multi_accept on;
}
# ==================== HTTP模块配置(http context)====================
# 这是Nginx作为HTTP服务器的核心配置区域
http {
# 包含MIME类型定义文件。MIME类型告诉浏览器如何处理不同类型的文件
# 例如:text/html, application/javascript, image/png 等
include /etc/nginx/mime.types;
# 默认的MIME类型。如果请求的文件没有匹配到任何MIME类型,则使用这个
default_type application/octet-stream;
# 开启高效文件传输模式(零拷贝技术)
# 文件传输会尽量使用直接内存访问,减少在内核态和用户态之间的数据拷贝
sendfile on;
# 配合sendfile使用。当连接保持活跃时,允许在一个连接中发送多个文件
tcp_nopush on;
# 保持连接的超时时间。客户端完成最后一次请求后,连接保持打开的时间
# 在这段时间内如果有新请求,可以复用这个连接,避免重复建立TCP连接
keepalive_timeout 65;
# 启用gzip压缩,减少传输数据量
gzip on;
# gzip压缩级别,1-9,数字越大压缩率越高但CPU消耗越大。通常4-6是平衡点
gzip_comp_level 5;
# 最小压缩文件大小,小于这个值不压缩(避免小文件压缩后反而更大)
gzip_min_length 256;
# 压缩类型,这些类型的文件适合压缩
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
# ==================== 服务器块配置(server context)====================
# 这是虚拟主机配置,每个server块可以配置一个网站/应用
# 可以包含多个server块来配置多个网站
server {
# 监听端口和绑定的主机名(IP地址)
# 监听80端口,IPv4和IPv6的所有地址
listen 80;
listen [::]:80;
# 服务器的域名。当通过这个域名访问时,这个server块生效
# 可以有多个,用空格分隔
server_name localhost;
# 网站根目录,即存放HTML/CSS/JS等文件的位置
root /usr/share/nginx/html;
# 默认索引文件。当访问目录时,Nginx会按顺序查找这些文件
index index.html index.htm;
# ==================== 位置块配置(location context)====================
# 针对特定URL路径的详细配置
# location [匹配模式] { ... }
# 匹配所有请求(最通用,优先级最低)
location / {
# 尝试访问请求的文件,如果不存在则返回404错误
try_files $uri $uri/ =404;
}
# 错误页面配置。当发生特定HTTP错误时,返回自定义页面
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
# 定义50x错误页面的位置
location = /50x.html {
root /usr/share/nginx/html;
}
# 禁止访问 .htaccess 等隐藏文件(Apache配置文件)
location ~ /\.ht {
deny all;
}
# 静态文件缓存配置示例(通常在单独配置文件中)
# location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
# expires 30d; # 浏览器缓存30天
# add_header Cache-Control "public, immutable";
# }
}
# ==================== 包含其他配置文件 ====================
# 引入其他配置文件,实现配置模块化
# 通常会把虚拟主机配置放在 /etc/nginx/conf.d/ 或 /etc/nginx/sites-enabled/
include /etc/nginx/conf.d/*.conf;
# include /etc/nginx/sites-enabled/*; # Ubuntu/Debian常用
}

接下来把前端打包后的dist文件夹上传到服务器中 修改配置 指定目录:

root /usr/services/dist;

修改配置将用户指定为当前使用的用户

user root

输入命令重载配置:

Terminal window
nginx -s reload

可以使用命令查看某匹配进程

Terminal window
ps -ef|grep 'nginx'

后端#

需要安装java、maven(如果需要在服务器打包) 使用系统软件包管理器命令

Terminal window
yam instll -y java-1.8.0-openjdk*

maven下载后解压 直接可以使用mvn可执行文件来执行命令 需要配置环境变量:

vim /etc/profile

shift+G 到最后一行 添加

export PATH=$PATH:/user/local/nginx/sbin:(maven路径)

获取后端代码:

  1. 使用git克隆云端仓库
  2. 使用mvn package -DskipTests //跳过测试
  3. 得到一个jar包
  4. 使用nohup java -jar ./jar包路径 --spring.profiles.active=prod & 执行后端 nohub和末尾的&可以让项目在后台运行,运行后会提示一个进程号
    使用jobs、netstat -ntlp、jps(查看所有java的)命令可以查看运行的进程
  5. 这里如果没有权限可以使用 chmod a+x jar包路径 a+x就是添加执行权限

前端web应用托管部署#

前端腾讯云web应用托管(比容器化更傻瓜式,不需要自己写构建应用的命令,就能启动前端项目)

  • 小缺点:需要将代码放到代码托管平台上

可以直接从仓库中导入项目,然后自动构建并运行部署。 image.png

每次修改代码推送都会重新自动构建

使用宝塔Linux部署#

相当于有一个Linux运维面板,可以可视化安装各种东西和配置各种东西。 官方安装教程: https://www.bt.cn/new/download.html 方便管理服务器、方便安装软件

安装Nginx 和java

然后在网站那添加一个站点 将dist文件拖进来进行上传 默认配置就能直接运行 image.png

java项目同样是添加后修改配置命令即可

注意要记得放行80和8080端口

Docker部署#

Docker是容器,可以将项目的环境和代码一起打包成镜像,更容易分发和移植。相当于将配置的步骤也全部用文件形式描述出来了。

Dockerfile用于指定构建Docker镜像的方法

Dockerfile一般情况下不需要完全从0自己写,建议去github、gitee等托管平台参考同类项目(比如springboot) Dockerfile编写:

  • FROM依赖的基础镜像
  • WORKDIR工作目录
  • COPY从本机复制文件
  • RUN执行命令
  • CMD/ENTRYPOINT(附加额外参数)指定运行容器时默认执行的命令

这里需要给前后端项目分别写一个dockerfile 然后运行 前端需要的是Nginx的配置 用来部署

后端需要的是maven的配置来构建项目 然后运行

Terminal window
// -t是打标签
docker build -t user-center-frontend:v0.0.1 .

之后使用docker run进行运行

Terminal window
docker run -p 80:80 -v /本机目录:/data -d 镜像名称

进入容器:

Terminal window
docker exec -it 镜像名 bash

查看容器日志:-f表示一直跟踪新日志

Terminal window
docker logs -f 容器名

查看进程

Terminal window
docker ps

杀死容器

Terminal window
docker kill 容器名

删除镜像:

Terminal window
docker rmi -f 镜像名

容器平台部署#

  1. 云服务商的容器平台(腾讯云、阿里云)
  2. 面向某个领域的容器平台(微信云托管)

容器平台的好处:

  1. 不用输命令来操作,更方便省事
  2. 不用在控制台操作,更傻瓜式、更简单
  3. 大广运维比自己运维更省心
  4. 额外的能力,比如监控、告警、其他(存储、负载均衡、自动扩缩容、流水线)

只需要上传dockerfile和各种东西直接确认部署即可。

绑定域名#

在购买域名那个地方 添加解析记录: image.png

前端项目访问: 用户输入网址 =>(防火墙) =>域名解析服务器(把网址解析为ip地址 / 交给其他域名解析服务) =>Nginx接收请求,找到对应文件,返回文件给前端 =>前端加载文件到浏览器中 =>渲染页面 (注意需要加域名到Nginx配置中,在宝塔面板中添加)

后端项目访问: 用户输入网址 =>域名解析服务器 =>服务器 =>nginx接收请求 =>后端项目(比如8080端口)

这里正常配置完之后,是需要访问 域名:8080 才可以访问到后端的

为了解决这个问题 可以使用Nginx来转发请求

添加Nginx反向代理 将本地: image.png

访问根目录时,自动转发到本地8080服务器 image.png

跨域问题解决#

浏览器为了用户的安全,仅允许向同域名、同端口的服务器发送请求

处理跨域问题:

  1. 把域名改成相同的 让服务器告诉浏览器:允许跨域(返回cross-orign-allow 响应头)
  2. 网关支持 Nginx网关配置:
# 跨域配置 匹配/api
location ^~ /api/ {
# 反向代理到本地的/api 位置
proxy_pass http://127.0.0.1:8080/api/;
# 添加允许跨域请求头
add_header 'Access-Control-Allow-Origin' $http_origin;
# 浏览器携带cookie
add_header 'Access-Control-Allow-Credentials' 'true';
# 允许方法
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers '*';
# 如果是OPTIONS请求则直接返回通过 需要加上各种响应头
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
  1. 修改后端服务(三种方式) 1.配置@CrossOrigin注解
/**
* 用户接口层 * * @author ShineAcZ
* @date 2025/12/28
*/@RestController
@RequestMapping("/user")
@CrossOrigin(origins = {"http://user.code-nav.cn"},allowCredentials = "true", methods = {RequestMethod.PUT,RequestMethod.GET})
public class UserController {
@Resource
UserService userService;

2.添加web全局请求拦截器

@Configuration
public class WebMvcConfg implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
//当 **Credentials为true时,** Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
.allowedOrigins("http://localhost:9527", "http://127.0.0.1:9527", "http://127.0.0.1:8082", "http://127.0.0.1:8083")
//是否允许证书 不再默认开启
.allowCredentials(true)
//设置允许的方法
.allowedMethods("*")
//跨域允许时间
.maxAge(3600);
}
}

3.定义新的 corsFilter Bean参考 : https://www.jianshu.com/p/b02099a435bd

项目优化点#

  1. 功能扩充
    1. 管理员创建用户、修改用户信息、删除用户
    2. 上传头像
    3. 条件查询
    4. 更改权限
  2. 修改Bug
  3. 项目登录改为分布式session(单点登录)目前是用的session cookie方式
  4. 通用性
    1. set-cookie domain 域名更通用,比如改为*.xxx.com
    2. 把用户管理系统=>用户中心(之后多个后端项目都请求这个后端)
  5. 后台添加全局请求拦截器(统一判断用户权限、统一记录请求日志)
用户中心项目学习
http://www.shineacz.top/posts/用户中心项目学习/
作者
shineAcZ
发布于
2025-12-22
许可协议
CC BY 4.0