YEGUO

用户中心开发

|
|
21 min read

一、技术选型

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

后端:

  • java
  • spring(依赖注入框架,帮助你管理 Java 对象,集成一些其他的内容)
  • springmvc(web 框架,提供接口访问、restful接口等能力)
  • druid连接池
  • mybatis(Java 操作数据库的框架,持久层框架,对 jdbc 的封装)
  • mybatis-plus(对 mybatis 的增强,不用写 sql 也能实现增删改查)
  • springboot(快速启动 / 快速集成项目。不用自己管理 spring 配置,不用自己整合各种框架)
  • junit 单元测试库
  • mysql 数据库

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

二、初始化项目

1.前端初始化

​ 1.初始化项目:使用umijs脚手架初始化项目√

​ 2.引入组件:√

​ 3.项目瘦身:删除不需要文件√

2.后端初始化

​ 1.初始化项目:使用springboot模板初始化,引入依赖,√

​ 2.数据库连接池:druid连接池整合√

​ 3.持久层工具:mybatis-plus整合√

​ 4.数据库连接:数据库连接测试√

​ 5.数据库表:数据库表设计√

三、后端

一、用户表设计

id(主键)bigint

username 昵称 varchar

user_account 登录账号

avatar_url 头像 varchar

gender 性别 tinyint

user_password 密码 varchar

phone 电话 varchar

email 邮箱 varchar

user_status 用户状态 int 0 - 正常

create_time 创建时间(数据插入时间)datetime

update_time 更新时间(数据更新时间)datetime

is_delete 是否删除 0 1(逻辑删除)tinyint

user_role 用户角色 0 - 普通用户 1 - 管理员

二、注册接口

/api/user/register POST

1.用户输入:用户在前端输入账号、密码、校验密码

2.后端校验:校验用户账号、密码、校验密码是否有效

  • 每个都不能为空 √
  • 账号长度不小于4位√
  • 密码不小于8位√
  • 账号不能包含特殊字符√
  • 密码和校验密码相同√
  • 账号不能重复√

3.密码加密:使用sm4算法密码加密√

4.插入数据库表:将该用户insert用户表中√

5.返回数据:对成功数据封装响应类返回

6.测试√ 校验错误处都抛出业务异常

三、登录接口

/api/user/login POST

1.用户输入:用户在前端输入账号、密码

2.后端校验:校验用户账号、密码是否有效

3.数据库查询:查询用户,如果user为空则返回

4.查询结果:存在,对前端密码加密和查询到的对比

5.密码正确:为用户设置session

6.返回数据:正确则脱除敏感信息后封装成响应类返回信息

7.测试√

校验错误处都抛出业务异常

四、其他接口

/api/user/current GET // 查询当前用户

/api/user/logout POST // 退出登录

/api/user/selectAll GET // 查询所有

/api/user/remove POST // 删除

…

五、封装常量类

public class UserConstant {
    /**
     * 用户登录态key
     */
    public static final String USER_LOGIN_STATE = "userLoginState";

    /**
     * 普通用户
     */
    public static final int DEFAULT_ROLE = 0;

    /**
     * 管理员
     */
    public static final int ADMIN_ROLE = 1;
}

六、封装通用类

1.封装通用响应类

public class Result<T> implements Serializable {

    private int code;

    private T data;

    private String message;

    private String description;

    public Result(int code, T data, String message) {
        this.code = code;
        this.data = data;
        this.message = message;
    }
    // 以下重载构造函数可以根据自己业务需要书写提供语法糖
     public Result(int code,T data) {
        this(code,data,"成功");
    }
    // 失败不返回数据
    public Result(ErrorCode errorCode){
        this(errorCode.getCode(),null, errorCode.getMessage());
    }
    public Result(ErrorCode errorCode,String description) {
        this.code = errorCode.getCode();
        this.data = null;
        this.message = errorCode.getMessage();
        this.description = description;
    }
    public Result(int code,T data,String message,String description) {
        this.code = code;
        this.data = data;
        this.message = message;
        this.description = description;
    }
}

2.封装工具类创建响应对象

public class ResultUtils {
    // 成功
    public static <T> Result<T> success(T data) {
        return new Result<>(20000,data);
    }

    // 失败
    public static <T> Result<T> err(ErrorCode errorCode) {
        return new Result<>(errorCode);
    }
	 // 以下重载构造函数可以根据自己业务需要书写提供语法糖
    public static <T> Result<T> err(ErrorCode errorCode,String description) {
        return new Result<>(errorCode,description);
    }

    public static <T> Result<T> err(int code,String message,String description) {
        return new Result<>(code,null,message,description);
    }
}

3.封装业务错误码类

public enum ErrorCode {

    NOT_LOGIN(40000,"未登录"),
    PARAMS_ERROR(40001,"请求参数错误"),
    USER_USED_ERROR(40002,"账号已存在"),
    NOT_AUTHORITY(40003,"无权限"),
    NOT_FOUND_ERROR(40004,"未找到"),
    PASSWORD_ERROR(40005,"密码错误"),
    SYSTEM_ERROR(50000,"运行时异常")
    ;


    private int code;

    private String message;
    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;

    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

七、封装业务异常类

业务返回错误处直接抛出异常,在全局异常处理器集中处理异常,并返回给前端

public class BusinessException extends RuntimeException{

    private final int code;
    private final String description;


    public BusinessException(int code, String message, String description) {
        super(message);
        this.code = code;
        this.description = description;
    }

    public BusinessException(ErrorCode errorCode, String description) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
        this.description = description;
    }

    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
        this.description = "";
    }

    public int getCode() {
        return code;
    }

    public String getDescription() {
        return description;
    }
}

八、全局异常处理器

使用spring提供@RestControllerAdvice注解,全局异常控制器处理抛出的异常,使用lombok的@slf4j来记录日志

@Slf4j
@RestControllerAdvice
public class GlobalException {

    @ExceptionHandler(BusinessException.class)
    public Result businessExceptionHandler(BusinessException e) {
        log.error("businessException:",e.getMessage(),e.getDescription());
        return ResultUtils.err(e.getCode(),e.getMessage(),e.getDescription());
    }

    @ExceptionHandler(RuntimeException.class)
    public Result runtimeExceptionHandler(RuntimeException e) {
        log.error("runtimeException:",e);
        return ResultUtils.err(ErrorCode.SYSTEM_ERROR,"运行时异常");
    }
}

九、全局拦截器

实现HandlerInterceptor接口实现preHandle()方法在请求处理前进行全局的处理

@Component
public class GlobalPreInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false); // 参数设置为false,如果Session不存在则返回null
        if (session == null || session.getAttribute(UserConstant.USER_LOGIN_STATE) == null) {
            // Session不存在或用户未登录,可以进行相应的处理
           throw new BusinessException(ErrorCode.NOT_LOGIN,"未登录");
        }
        return true;
    }
}

十、注册拦截器

将全局拦截器注册到配置类中

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private GlobalPreInterceptor globalPreInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(globalPreInterceptor)
                .addPathPatterns("/**") // 拦截所有路径
                .excludePathPatterns("/user/login","/user/register",
                        "/user/logout"); // 不拦截登录路径
    }
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowCredentials(true)
                .allowedOriginPatterns("*")
                .allowedMethods("*")
                .allowedHeaders("*")
                .maxAge(3600); // 允许携带身份验证信息
    }
}

十一、多环境

springboot可以根据配置文件后缀识别当前环境,application.yml为基本配置,启用某个环境,环境中与基本配置中相同的属性会覆盖掉,其他的会使用基本配置. application.yml,application-production.yml,application-test.yml多个配置文件 可通过以下命令来启用某环境配置

java -jar xxxx.jar --spring.profiles.active=production
//启用生产环境配置

四、前端

一、umijs

1.app.tsx 运行时配置 getInitialState()函数

  • 每次初始化getInitialState()会查询当前用户,将一些返回值存入InitialState,全局可取

  • 每次刷新页面都会执行getInitialState()

  • 切换页面不执行

layout: RunTimeLayoutConfig

  • onPageChange()只要改变有路由的页面就会执行,无论手动还是页面的跳转

二、目录结构

.
├── config
│   └── config.ts
├── dist
├── mock
│   └── app.ts|tsx
├── src
│   ├── .umi
│   ├── .umi-production
│   ├── layouts
│   │   ├── BasicLayout.tsx
│   │   ├── index.less
│   ├── models
│   │   ├── global.ts
│   │   └── index.ts
│   ├── pages
│   │   ├── index.less
│   │   └── index.tsx
│   ├── utils 
│   │   └── index.ts
│   ├── services 
│   │   └── api.ts
│   ├── app.(ts|tsx)
│   ├── global.ts
│   ├── global.(css|less|sass|scss)
│   ├── overrides.(css|less|sass|scss)
│   ├── favicon.(ico|gif|png|jpg|jpeg|svg|avif|webp)
│   └── loading.(tsx|jsx)
├── node_modules
│   └── .cache
│       ├── bundler-webpack
│       ├── mfsu
│       └── mfsu-deps
├── .env
├── plugin.ts 
├── .umirc.ts 
├── package.json
├── tsconfig.json
└── typings.d.ts

三、配置文件

umi max提供了很多插件如

export default defineConfig({
	routes,//路由的配置
 	fastRefresh: true,//快速热更新配置
 	model: {},//数据流插件
 	initialState: {},//一个全局的初始数据流,可以用它在插件之间共享数据
	antd: {},//antd 插件
	request: {},//网络请求配置
	access: {},//权限插件
	headScripts: [
    // 解决首次加载时白屏的问题
    {
      src: '/scripts/loading.js',
      async: true,
    },
  ],
    mfsu: {//打包提速
    strategy: 'normal',
  },
  proxy: proxy.dev//代理
})

四、配置代理

将带有/api前缀请求代理到后端地址解决开发时跨域问题,build后失效

export default proxyConfig{
	dev: {
    '/api/': {
      target: 'http://localhost:8080/',
      changeOrigin: true,
    },
  },
}

五、命名空间API内定义类型

如定义响应、注册params、注册响应数据类型

declare namespace API {
    type Response<T> = {
       code: number;
       data: T;
       message: string; 
       description?: string;
    },
    type RegisterParams={
        userAccount:string;
        userPassword:string;
        checkPassword:string;
        type?: string;
  	},
    type RegisterResult = number;
    type CurrentUser = {
        userRole: number;
        ...//其他属性
    }
}

六、封装umi提供的request接口

如下

export async function register(body: API.RegisterParams, options?: { [key: string]: any }) {
  return request<API.Response<API.RegisterResult>>('/api/user/register', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    data: body,
    ...(options || {}),
  });
}
...//其他接口

七、request配置多环境

可以配置请求拦截器和相应拦截器,来进行个性化处理,可以配置根据当前环境更改基础请求地址,方便开发

export const request = {
  baseURL: process.env.NODE_ENV==='production'?"https://production.envriment.com":"http://localhost:8080",
  // 请求拦截器
  requestInterceptors: [
    (config) => {
    // 拦截请求配置,进行个性化处理。
      const url = config.url.concat('?token = 123');
      return { ...config, url};
    }
  ],

  // 响应拦截器
  responseInterceptors: [
    (response) => {
       // 拦截响应数据,进行个性化处理
       const { data } = response;
       if(!data.success){
         message.error('请求失败!');
       }
       return response;
    }
  ]
};

八、权限管理

/src/access.ts是默认的权限定义文件

// /src/access.ts
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
  const { currentUser } = initialState ?? {};
  return {
    canAdmin: currentUser && currentUser.userRole === 1,
  };
}

路由配置文件中,只有是canAdmin才能访问/admin路径下页面

export default[
    { path: '/welcome', name: '欢迎', icon: 'smile', component: './Welcome' },
    {
        path: '/admin',
        name: '管理页',
        icon: 'crown',
        access: 'canAdmin', // 只有canAdmin能访问
        routes: [
          { path: '/admin', redirect: '/admin/user-manage' },
        ],
      },
]

九、加入注册、用户管理页面

/src/pages/目录是页面目录,在该目录下加入自己需要的页面。需要其他组件在/src/components/ 目录下进行开发或引入

五、前后端联调

对已有接口进行前后端联调,检查bug,解决

六、前后端分离,跨域解决

一、nginx配置代理解决跨域

location ^~ /api/ { 
    proxy_pass http://127.0.0.1:8080/api/;
    add_header 'Access-Control-Allow-Origin' $http_origin; 
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; 
    add_header Access-Control-Allow-Headers '*'; 
    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.配置 @CrossOrigin 注解

@CrossOrigin(origins = { "http://xxx.com"}, methods = { RequestMethod.DELETE },allowCredentials = "true")
  • origins: 同样是指定允许跨域请求的源,即允许访问资源的域名。在这里,同样只允许来自 “http://xxx.com” 的跨域请求。
  • methods: 允许请求的方法。
  • allowCredentials: 指定是否允许发送身份验证信息(例如 cookie)到目标域。在这里设置为 true,表示允许发送身份验证信息。

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://127.0.0.1:9527", 
           		"http://127.0.0.1:8082",
                "http://127.0.0.1:8083")
                //是否允许证书 不再默认开启 
                .allowCredentials(true) 
                //设置允许的方法 
                .allowedMethods("*")
                //跨域允许时间 
                .maxAge(3600);
    }
    // 或
     @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            //当 Credentials为true时,Origin不能为星号,需为 具体的ip地址【如果接口不带cookie,ip无需设成具体ip】,使用allowedOriginPatterns可以带*
                .allowedOriginPatterns("*")
           		 //设置允许的方法 
                .allowedMethods("*")
               	//指定是否允许发送身份验证信息(例如 cookie)到目标域
                .allowCredentials(true)
             	//跨域允许时间 
            	.maxAge(3600); // 允许携带身份验证信息
	}

}

七、nginx配置ssl

这个配置文件最后引入到nginx.conf主配置文件的http区域中

server {
	#SSL 默认访问端口号为 443
	listen 443 ssl;
	#请填写绑定证书的域名
	server_name example.com; 
 
	# 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;
	index index.html index.htm;
	include /etc/nginx/mime.types;
    	  
	#请填写证书文件的相对路径或绝对路径 注意容器nginx配置路径是挂载对应目录
	ssl_certificate /ssl/domain_bundle.crt; 
	#请填写私钥文件的相对路径或绝对路径
	ssl_certificate_key /ssl/domain.key; 
	ssl_session_timeout 5m;
	#请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。
	ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;

	#请按照以下协议配置
	ssl_protocols TLSv1.2 TLSv1.3;
	ssl_prefer_server_ciphers on;
    // 网关支持
    location ^~ /api/ { 
		rewrite ^/api(.*)$ $1 break;
		proxy_pass http://172.17.0.1:8080/;
		add_header 'Access-Control-Allow-Origin' $http_origin; 
		add_header 'Access-Control-Allow-Credentials' 'true';
		add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; 
		add_header Access-Control-Allow-Headers '*'; 
		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; 
} 
}
    location / {
		try_files $uri /index.html;
    }
    if ($host != "example.com") {  
        return 403;  # 如果请求的域名不匹配,返回 403 禁止访问
    }

}

server {
	listen 80;
	#请填写绑定证书的域名
	server_name example; 
	#把http的域名请求转成https
	return 301 https://$host$request_uri; 
}

八、多方式部署上线

一、原生部署

1.前端:在服务器端手动安装nginx

或者:centos直接用yum下载不需要下面这么麻烦,

使用yum直接下载nginx,将构建的dist目录放在配置的root目录下 √

# 下载 nginx curl -o nginx-1.21.6.tar.gz http://nginx.org/download/nginx-1.21.6.tar.gz # 解压 nginx tar -zxvf nginx-1.21.6.tar.gz # 进入 nginx 目录
cd nginx-1.21.6 # 安装 nginx 需要的环境 
yum install pcre pcre-devel -y
yum install openssl openssl-devel -y
yum -y install gcc-c++
# 配置文件检查 
./configure --with-http_ssl_module --with-http_v2_module --with-stream
# 编译 
make 
make install
# 查看是否有 nginx 的可执行文件
ls /usr/local/nginx/sbin/nginx
# 配置全局环境变量, shift+g 定位到最后一行,再按 o 在最后一行插入
vim /etc/profile 
#在最后一行添加:export PATH=$PATH:/usr/local/nginx/sbin 
#修改环境变量后执行:source /etc/profile
# 启动nginx
nginx 
# 查看启动情况 
netstat -ntlp #查看启动情况 
# 修改 nginx 配置后,需要运行如下命令,才生效
nginx -s reload

#####
#补充:后端打包后。将编译好的文件压缩为zip
#使用命令解压压缩文件到指定目录
unzip dist.zip -d your_project_directory
#随后进入其中的dist目录使用命令
mv * ../
#将dist目录的所有文件移至外层即your_project_directory目录,并对dist文件夹进行删除
#随后我们对nginx.conf配置文件进行更改
#tips好习惯:在对配置文件进行更改的时候,先对配置文件进行备份
cp nginx.conf nginx.default.conf
#使用自带的vim编译器进行编辑
vim nginx.conf
#tips :报错退出  esc + :wq    
#i 插入模式  o 新增一行
#主要对user进行编辑,修改默认值为root,同时修改lcoation下的root 后为我们解压好的目录
#随后移动这个文件
cp nginx.conf /usr/local/nginx
mv nginx.conf ./conf/
#随后开启nginx服务器,访问服务器的默认公网地址即可
nginx

后端:下载好java,将打包好的jar传到linux服务器,linux下载java,直接命令启动√

// 运行 jar 包
jar -jar ./Your_project.jar --server.port=8080 -spring.profiles.active=production
// 后台运行
# 后台运行 jar 包
nohup java -jar ./Your_project.jar --server.port=8080 -- spring.profiles.active=production & 

jps:查看所有运行的java程序

二、宝塔面板部署

非常方便简单

三、docker容器部署

前端

1.Dockerfile 根据自己需求书写
// Dockerfile
# 使用 NGINX 官方提供的稳定版作为基础镜像
FROM nginx:stable

# 暴露 NGINX 默认的 HTTP 端口
EXPOSE 80

# 在容器启动时自动启动 NGINX 服务
CMD ["nginx", "-g", "daemon off;"]
2.构建镜像,运行
  • 将dockerfile、dist、nginx配置文件放在目录中进行构建镜像

  • 构建镜像:docker build -t project_name:version .

  • 运行容器配置挂载目录,宿主机挂载目录根据自己需求而定

  • docker run -id --name=Your_project_name -p 80:80 -v /service/your_project/dist:/usr/share/nginx/html  -v /service/your_project/logs:/var/log/nginx  -v /service/your_project/nginx:/etc/nginx/conf.d -v 
    /service/your_project/ssl:/ssl 镜像:版本

后端

1.Dockerfile 根据自己需求书写
# 使用 maven jdk8作为基础镜像
FROM maven:3.5-jdk-8-alpine as builder

# 指定工作目录/app,cp pom.xml和src ==》 /app目录下
WORKDIR /app
COPY pom.xml .
COPY src ./src

# maven打包跳过测试
RUN mvn package -DskipTests

# 运行,指定环境
CMD ["java","-jar","/app/target/your_project.jar","--spring.profiles.active=production"]
2.构建镜像,运行
  • 如果项目在github仓库中直接clone过来,否则自己打包上传

  • 构建镜像:docker build -t project_name:version .

  • 运行容器配置挂载目录,宿主机挂载目录根据自己需求而定

  • docker run -id --name=Your_project_name  镜像:版本

四、云平台托管

云服务商容器平台(腾讯、阿里、微信云)等,可快速部署,更省心省力,注意收费规则

容器平台的好处:

  1. 不用输命令来操作,更方便省事

  2. 不用在控制台操作,更傻瓜式、更简单

  3. 大厂运维,比自己运维更省心

  4. 额外的能力,比如监控、告警、其他(存储、负载均衡、自动扩缩容、流水线)

九、域名绑定,开放端口

一、注册域名

到各大运营商(腾讯、阿里等)注册想要的域名

二、域名解析

将域名解析到服务器地址,nginx绑定相应server_name,如果是云托管平台,使用CNAME解析到平台提供的地址上

三、服务器防火墙开放相应端口

© 2023 - 2026 YEGUO