0%

尚硅谷尚庭公寓-03

尚硅谷尚庭公寓-03

ThreadLocal

介绍

ThreadLocal是JDK提供的一个线程内部的数据存储工具类,它提供了一些方法,用于在当前线程中存储数据,并且每个线程都可以独立地访问自己的数据。

实践

  • 理论上我们可以在Controller方法中,使用@RequestHeader获取JWT,然后在进行解析,如下

    1
    2
    3
    4
    5
    6
    7
    8
    @Operation(summary = "获取登陆用户个人信息")
    @GetMapping("info")
    public Result<SystemUserInfoVo> info(@RequestHeader("access-token") String token) {
    Claims claims = JwtUtil.parseToken(token);
    Long userId = claims.get("userId", Long.class);
    SystemUserInfoVo userInfo = service.getLoginUserInfo(userId);
    return Result.ok(userInfo);
    }

    上述代码的逻辑没有任何问题,但是这样做,JWT会被重复解析两次(一次在拦截器中,一次在该方法中)。为避免重复解析,通常会在拦截器将Token解析完毕后,将结果保存至ThreadLocal中,这样一来,我们便可以在整个请求的处理流程中进行访问了。

    common模块中创建com.atguigu.lease.common.login.LoginUserHolder工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class LoginUserHolder {
    public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();

    public static void setLoginUser(LoginUser loginUser) {
    threadLocal.set(loginUser);
    }

    public static LoginUser getLoginUser() {
    return threadLocal.get();
    }

    public static void clear() {
    threadLocal.remove();
    }
    }

    同时在common模块中创建com.atguigu.lease.common.login.LoginUser

    1
    2
    3
    4
    5
    6
    7
    @Data
    @AllArgsConstructor
    public class LoginUser {

    private Long userId;
    private String username;
    }
  • 修改AuthenticationInterceptor拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Component
    public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    String token = request.getHeader("access-token");

    Claims claims = JwtUtil.parseToken(token);
    Long userId = claims.get("userId", Long.class);
    String username = claims.get("username", String.class);
    LoginUserHolder.setLoginUser(new LoginUser(userId, username));

    return true;

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    LoginUserHolder.clear();
    }
    }
  • 编写Controller层逻辑

    LoginController中增加如下内容

    1
    2
    3
    4
    5
    6
    @Operation(summary = "获取登陆用户个人信息")
    @GetMapping("info")
    public Result<SystemUserInfoVo> info() {
    SystemUserInfoVo userInfo = service.getLoginUserInfo(LoginUserHolder.getLoginUser().getUserId());
    return Result.ok(userInfo);
    }
  • 编写Service层逻辑

    LoginService中增加如下内容

    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    public SystemUserInfoVo getLoginUserInfo(Long userId) {
    SystemUser systemUser = systemUserMapper.selectById(userId);
    SystemUserInfoVo systemUserInfoVo = new SystemUserInfoVo();
    systemUserInfoVo.setName(systemUser.getName());
    systemUserInfoVo.setAvatarUrl(systemUser.getAvatarUrl());
    return systemUserInfoVo;
    }

xml文件<>的转义

由于xml文件中的<>是特殊符号,需要转义处理。

原符号 转义符号
< &lt;
> &gt;

Mybatis-Plus分页插件注意事项

使用Mybatis-Plus的分页插件进行分页查询时,如果结果需要使用<collection>进行映射,只能使用嵌套查询(Nested Select for Collection),而不能使用嵌套结果映射(Nested Results for Collection)
嵌套查询嵌套结果映射是Collection映射的两种方式,下面通过一个案例进行介绍
例如有room_infograph_info两张表,其关系为一对多,如下

现需要查询房间列表及其图片信息,期望返回的结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[
{
"id": 1,
"number": 201,
"rent": 2000,
"graphList": [
{
"id": 1,
"url": "http://",
"roomId": 1
},
{
"id": 2,
"url": "http://",
"roomId": 1
}
]
},
{
"id": 2,
"number": 202,
"rent": 3000,
"graphList": [
{
"id": 3,
"url": "http://",
"roomId": 2
},
{
"id": 4,
"url": "http://",
"roomId": 2
}
]
}
]

为得到上述结果,可使用以下两种方式

  • 嵌套结果映射

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <select id="selectRoomPage" resultMap="RoomPageMap">
    select ri.id room_id,
    ri.number,
    ri.rent,
    gi.id graph_id,
    gi.url,
    gi.room_id
    from room_info ri
    left join graph_info gi on ri.id=gi.room_id
    </select>

    <resultMap id="RoomPageMap" type="RoomInfoVo" autoMapping="true">
    <id column="room_id" property="id"/>
    <collection property="graphInfoList" ofType="GraphInfo" autoMapping="true">
    <id column="graph_id" property="id"/>
    </collection>
    </resultMap>

    这种方式的执行原理如下图所示

  • 嵌套查询

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <select id="selectRoomPage" resultMap="RoomPageMap">
    select id,
    number,
    rent
    from room_info
    </select>

    <resultMap id="RoomPageMap" type="RoomInfoVo" autoMapping="true">
    <id column="id" property="id"/>
    <collection property="graphInfoList" ofType="GraphInfo" select="selectGraphByRoomId" column="id"/>
    </resultMap>

    <select id="selectGraphByRoomId" resultType="GraphInfo">
    select id,
    url,
    room_id
    from graph_info
    where room_id = #{id}
    </select>

    这种方法使用两个独立的查询语句来获取一对多关系的数据。首先,Mybatis会执行主查询来获取room_info列表,然后对于每个room_info,Mybatis都会执行一次子查询来获取其对应的graph_info

    若现在使用MybatisPlus的分页插件进行分页查询,假如查询的内容是第1页,每页2条记录,则上述两种方式的查询结果分别是

  • 嵌套结果映射

  • 嵌套查询

    显然嵌套结果映射的分页逻辑是存在问题的。

异步操作

  • 保存浏览历史的动作不应影响前端获取房间详情信息,故此处采取异步操作。Spring Boot提供了@Async注解来完成异步操作,具体使用方式为:

    • 启用Spring Boot异步操作支持
      在 Spring Boot 主应用程序类上添加 @EnableAsync 注解,如下

      1
      2
      3
      4
      5
      6
      7
      @SpringBootApplication
      @EnableAsync
      public class AppWebApplication {
      public static void main(String[] args) {
      SpringApplication.run(AppWebApplication.class);
      }
      }
    • 在要进行异步处理的方法上添加 @Async 注解,如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      @Override
      @Async
      public void saveHistory(Long userId, Long roomId) {

      LambdaQueryWrapper<BrowsingHistory> queryWrapper = new LambdaQueryWrapper<>();
      queryWrapper.eq(BrowsingHistory::getUserId, userId);
      queryWrapper.eq(BrowsingHistory::getRoomId, roomId);
      BrowsingHistory browsingHistory = browsingHistoryMapper.selectOne(queryWrapper);

      if (browsingHistory != null) {
      browsingHistory.setBrowseTime(new Date());
      browsingHistoryMapper.updateById(browsingHistory);
      } else {
      BrowsingHistory newBrowsingHistory = new BrowsingHistory();
      newBrowsingHistory.setUserId(userId);
      newBrowsingHistory.setRoomId(roomId);
      newBrowsingHistory.setBrowseTime(new Date());
      browsingHistoryMapper.insert(newBrowsingHistory);
      }
      }

缓存优化

概述

缓存优化是一个性价比很高的优化手段,多数情况下,缓存优化可以通过一些简单的操作,换来性能的大幅提升。缓存优化的核心思想就是将一些原本保存在磁盘(例如MySQL)中的、经常访问并且查询开销比较大的数据,临时保存到内存(例如Redis)中。后序再访问相同数据时,就可直接从内存中获取结果,而无需再访问磁盘,由于内存的读写速度远高于磁盘,因此就能极大的提高程序的性能。

在使用缓存优化时,有一个问题不得不提,那就是数据库和缓存数据的一致性,当数据库中的数据发生变化时,缓存中的数据也要同步更新,否则就会出现数据不一致的问题,解决该问题的方案有如下几个

  • 数据发生变化时,更新数据库的同时也更新缓存
  • 数据发生变化时,更新数据库的同时删除缓存

在了解了缓存优化的核心思想后,我们以移动端中的根据ID获取房间详情接口为例,进行缓存优化。该接口涉及多表查询,查询时会多次访问数据库,查询代价较高,故可采取缓存优化,加快查询速度。

编写缓存逻辑

1.自定义RedisTemplate

本项目使用Reids保存缓存数据,因此我们需要使用RedisTemplate进行读写操作。前文提到过,Spring-data-redis提供了StringRedisTemplateRedisTemplate<Object,Object>两个实例,但是两个实例均不满足我们当前的需求,所以我们需要自定义RedisTemplate。

common模块中创建com.atguigu.lease.common.redis.RedisConfiguration类,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class RedisConfiguration {

@Bean
public RedisTemplate<String, Object> stringObjectRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(RedisSerializer.string());
template.setValueSerializer(RedisSerializer.java());
return template;
}
}

2.编写缓存逻辑

修改web-app模块中的com.atguigu.lease.web.app.service.impl.RoomInfoServiceImpl中的getDetailById方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override
public RoomDetailVo getDetailById(Long id) {

String key = RedisConstant.APP_ROOM_PREFIX + id;
RoomDetailVo roomDetailVo = (RoomDetailVo) redisTemplate.opsForValue().get(key);
if (roomDetailVo == null) {
//1.查询房间信息
......

//2.查询图片
......

//3.查询租期
......

//4.查询配套
......

//5.查询标签
......

//6.查询支付方式
......

//7.查询基本属性
......

//8.查询杂费信息
......

//9.查询公寓信息
......

roomDetailVo = new RoomDetailVo();
......

redisTemplate.opsForValue().set(key, roomDetailVo);
}

//10.保存浏览历史
browsingHistoryService.saveHistory(LoginUserHolder.getLoginUser().getUserId(), id);

return roomDetailVo;
}

3.编写删除缓存逻辑

为保证缓存数据的一致性,在房间信息发生变化时,需要删除相关缓存。
修改web-admin模块中的com.atguigu.lease.web.admin.service.impl.RoomInfoServiceImpl中的saveOrUpdateRoom方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Override
public void saveOrUpdateRoom(RoomSubmitVo roomSubmitVo) {
boolean isUpdate = roomSubmitVo.getId() != null;
super.saveOrUpdate(roomSubmitVo);

//若为更新操作,则先删除与Room相关的各项信息列表
if (isUpdate) {
//1.删除原有graphInfoList
......

//2.删除原有roomAttrValueList
......

//3.删除原有roomFacilityList
......

//4.删除原有roomLabelList
......

//5.删除原有paymentTypeList
......

//6.删除原有leaseTermList
......

//7.删除缓存
redisTemplate.delete(RedisConstant.APP_LOGIN_PREFIX + roomSubmitVo.getId());
}

//1.保存新的graphInfoList
......

//2.保存新的roomAttrValueList
......

//3.保存新的facilityInfoList
......

//4.保存新的labelInfoList
......

//5.保存新的paymentTypeList
......

//6.保存新的leaseTermList
......
}

修改web-admin模块中的com.atguigu.lease.web.admin.service.impl.RoomInfoServiceImpl中的removeRoomById方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public void removeRoomById(Long id) {

//1.删除RoomInfo
......

//2.删除graphInfoList
......

//3.删除attrValueList
......

//4.删除facilityInfoList
......

//5.删除labelInfoList
......

//6.删除paymentTypeList
......

//7.删除leaseTermList
......

//8.删除缓存
redisTemplate.delete(RedisConstant.APP_ROOM_PREFIX + id);
}

压力测试

可使用Postman或者Apifox等工具对根据ID获取房间详情这个接口进行压力测试,下图是增加缓存前后的测试报告

阿里云短信

获取短信验证码

该接口需向登录手机号码发送短信验证码,各大云服务厂商都提供短信服务,本项目使用阿里云完成短信验证码功能,下面介绍具体配置。

  • 配置短信服务

    • 开通短信服务

      • 阿里云官网,注册阿里云账号,并按照指引,完成实名认证(不认证,无法购买服务)

      • 找到短信服务,选择免费开通

      • 进入短信服务控制台,选择快速学习和测试

      • 找到发送测试下的API发送测试,绑定测试用的手机号(只有绑定的手机号码才能收到测试短信),然后配置短信签名和短信模版,这里选择[专用]测试签名/模版

    • 创建AccessKey

      云账号 AccessKey 是访问阿里云 API 的密钥,没有AccessKey无法调用短信服务。点击页面右上角的头像,选择AccessKey管理,然后创建AccessKey

      image-20230808104345383

  • 配置所需依赖

    如需调用阿里云的短信服务,需使用其提供的SDK,具体可参考官方文档

    common模块的pom.xml文件中增加如下内容

    1
    2
    3
    4
    <dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>dysmsapi20170525</artifactId>
    </dependency>
  • 配置发送短信客户端

    • application.yml中增加如下内容

      1
      2
      3
      4
      5
      aliyun:
      sms:
      access-key-id: <access-key-id>
      access-key-secret: <access-key-secret>
      endpoint: dysmsapi.aliyuncs.com

      注意

      上述access-key-idaccess-key-secret需根据实际情况进行修改。

    • common模块中创建com.atguigu.lease.common.sms.AliyunSMSProperties类,内容如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Data
      @ConfigurationProperties(prefix = "aliyun.sms")
      public class AliyunSMSProperties {

      private String accessKeyId;

      private String accessKeySecret;

      private String endpoint;
      }
    • common模块中创建com.atguigu.lease.common.sms.AliyunSmsConfiguration类,内容如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      @Configuration
      @EnableConfigurationProperties(AliyunSMSProperties.class)
      @ConditionalOnProperty(name = "aliyun.sms.endpoint")
      public class AliyunSMSConfiguration {

      @Autowired
      private AliyunSMSProperties properties;

      @Bean
      public Client smsClient() {
      Config config = new Config();
      config.setAccessKeyId(properties.getAccessKeyId());
      config.setAccessKeySecret(properties.getAccessKeySecret());
      config.setEndpoint(properties.getEndpoint());
      try {
      return new Client(config);
      } catch (Exception e) {
      throw new RuntimeException(e);
      }

      }
      }
  • 配置Redis连接参数

    1
    2
    3
    4
    5
    6
    spring: 
    data:
    redis:
    host: 192.168.10.101
    port: 6379
    database: 0
  • 编写Controller层逻辑

    LoginController中增加如下内容

    1
    2
    3
    4
    5
    6
    @GetMapping("login/getCode")
    @Operation(summary = "获取短信验证码")
    public Result getCode(@RequestParam String phone) {
    service.getSMSCode(phone);
    return Result.ok();
    }
  • 编写Service层逻辑

    • 编写发送短信逻辑

      • SmsService中增加如下内容

        1
        void sendCode(String phone, String verifyCode);
      • SmsServiceImpl中增加如下内容

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        @Override
        public void sendCode(String phone, String code) {

        SendSmsRequest smsRequest = new SendSmsRequest();
        smsRequest.setPhoneNumbers(phone);
        smsRequest.setSignName("阿里云短信测试");
        smsRequest.setTemplateCode("SMS_154950909");
        smsRequest.setTemplateParam("{\"code\":\"" + code + "\"}");
        try {
        client.sendSms(smsRequest);
        } catch (Exception e) {
        throw new RuntimeException(e);
        }
        }
    • 编写生成随机验证码逻辑

      common模块中创建com.atguigu.lease.common.utils.VerifyCodeUtil类,内容如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class VerifyCodeUtil {
      public static String getVerifyCode(int length) {
      StringBuilder builder = new StringBuilder();
      Random random = new Random();
      for (int i = 0; i < length; i++) {
      builder.append(random.nextInt(10));
      }
      return builder.toString();
      }
      }
    • 编写获取短信验证码逻辑

      • LoginServcie中增加如下内容

        1
        void getSMSCode(String phone);
      • LoginServiceImpl中增加如下内容

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        @Override
        public void getSMSCode(String phone) {

        //1. 检查手机号码是否为空
        if (!StringUtils.hasText(phone)) {
        throw new LeaseException(ResultCodeEnum.APP_LOGIN_PHONE_EMPTY);
        }

        //2. 检查Redis中是否已经存在该手机号码的key
        String key = RedisConstant.APP_LOGIN_PREFIX + phone;
        boolean hasKey = redisTemplate.hasKey(key);
        if (hasKey) {
        //若存在,则检查其存在的时间
        Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS);
        if (RedisConstant.APP_LOGIN_CODE_TTL_SEC - expire < RedisConstant.APP_LOGIN_CODE_RESEND_TIME_SEC) {
        //若存在时间不足一分钟,响应发送过于频繁
        throw new LeaseException(ResultCodeEnum.APP_SEND_SMS_TOO_OFTEN);
        }
        }

        //3.发送短信,并将验证码存入Redis
        String verifyCode = VerifyCodeUtil.getVerifyCode(6);
        smsService.sendCode(phone, verifyCode);
        redisTemplate.opsForValue().set(key, verifyCode, RedisConstant.APP_LOGIN_CODE_TTL_SEC, TimeUnit.SECONDS);
        }

        注意

        需要注意防止频繁发送短信。

部署

部署后端项目

打包

使用IDEA的maven插件对项目进行打包,完成后,在web-adminweb-app模块的target目录下找到web-admin-1.0-SNAPSHOT.jarweb-app-1.0-SNAPSHOT.jar

安装JDK

根据前文的部署方案,需要在server01部署后端服务,因此需要在server01中安装JDK,本项目采用JDK17。

  1. 获取JDK安装包

将资料中提前下载好的JDK上传到server01,也在服务器执行以下命令可直接下载。

1
wget https://download.oracle.com/java/17/archive/jdk-17.0.8_linux-x64_bin.tar.gz
  1. 解压JDK安装包

执行以下命令将jdk解压到/opt目录

1
tar -zxvf jdk-17.0.8_linux-x64_bin.tar.gz -C /opt
  1. 测试JDK安装效果

执行以下命令,观察输出是否正常

1
/opt/jdk-17.0.8/bin/java -version

部署

  1. 上传jar包

    将后端项目的两个jar包上传到server01服务器的/opt/lease目录下,若目录不存在,自行创建即可。

  2. 集成Systemd

    为方便项目的启动、停止或者重启,我们同样使用Systemd来管理后端服务的进程。

    • 移动端集成Systemd

      创建lease-app.service文件

      1
      vim /etc/systemd/system/lease-app.service

      内容如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      [Unit]
      Description=lease-app
      After=syslog.target

      [Service]
      User=root
      ExecStart=/opt/jdk-17.0.8/bin/java -jar /opt/lease/web-app-1.0-SNAPSHOT.jar 1>/opt/lease/app.log 2>&1
      SuccessExitStatus=143

      [Install]
      WantedBy=multi-user.target
    • 后台管理系统集成Systemd

      创建lease-admin.service文件

      1
      vim /etc/systemd/system/lease-admin.service

      内容如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      [Unit]
      Description=lease-admin
      After=syslog.target

      [Service]
      User=root
      ExecStart=/opt/jdk-17.0.8/bin/java -jar /opt/lease/web-admin-1.0-SNAPSHOT.jar 1>/opt/lease/admin.log 2>&1
      SuccessExitStatus=143

      [Install]
      WantedBy=multi-user.target
  3. 启动项目

    执行以下命令启动两个后端项目。

    1
    2
    systemctl start lease-app
    systemctl start lease-admin

部署前端项目

Nginx配置概述

移动端和后台管理系统的前端项目均部署在server02的Nginx中,Nginx的配置思路如下图所示

移动端

打包
  1. 明确前端请求的后端接口地址

    打包之前需要明确前端请求的后台接口地址,根据前文的部署规划,前端请求后台接口时走的是Ngxin反向代理,也就是请求的地址为http://81.68.xxx.xxx:xxx

    所以我们需要修改.env.production文件中VITE_APP_BASE_URL环境变量的值,修改结果如下

    1
    VITE_APP_BASE_URL='http://81.68.xxx.xxx:xxx'
  2. 构建项目

    在项目的根目录执行以下命令

    1
    npm run build
  3. 查看打包结果

    观察项目的根目录是否出现dist目录

部署
  1. 上传dist文件

    rentHouseH5项目编译得到dist文件上传至server02服务器的/usr/share/nginx/html/app目录下。

    最终的目录结构为

    1
    2
    3
    4
    5
    6
    7
    8
    /usr
    └── share
    └── nginx
    └── html
    └── app
    ├── static
    └── index.html
    └── ...
  2. 编辑Nginx配置文件

    创建/etc/nginx/conf.d/app.conf文件

    1
    vim /etc/nginx/conf.d/app.conf

    内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    server {
    listen xxxx;
    server_name 81.68.xxx.xxx;

    location / {
    root /usr/share/nginx/html/app;
    index index.html;
    }
    location /app {
    proxy_pass http://81.68.xxx.xxx:xxxx;
    }
    }
  3. 重新加载Nginx配置文件

    执行以下命令重新加载配置文件

    1
    systemctl reload nginx
  4. 访问项目

    访问http://xxxxxx

后台管理系统

打包
  1. 明确前端请求的后端接口地址

    后台管理系统的前端请求后端接口时,同样会走Nginx反向代理,故其请求的接口地址为http://81.68.xxx.xxx:xxxx

    确保rentHouseAdmin项目中的.env.production文件中的VITE_APP_BASE_URL环境变量配置为如下内容

    1
    VITE_APP_BASE_URL='http://81.68.xxx.xxx:xxxx'
  2. 打包

    在项目根目录执行以下命令

    1
    npm run build
  3. 查看打包结果

    观察项目的根目录是否出现dist目录

部署
  1. 上传dist文件

    rentHouseAdmin项目编译得到dist文件上传至server02服务器的/usr/share/nginx/html/admin目录下。

    最终的目录结构为

    1
    2
    3
    4
    5
    6
    7
    8
    /usr
    └── share
    └── nginx
    └── html
    └── admin
    ├── assets
    └── index.html
    └── ...
  2. 编辑Nginx配置文件

    创建/etc/nginx/conf.d/admin.conf文件

    1
    vim /etc/nginx/conf.d/admin.conf

    内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    server {
    listen xxxx;
    server_name 81.68.xxx.xxx;

    location / {
    root /usr/share/nginx/html/admin;
    index index.html;
    }
    location /admin {
    proxy_pass http://81.68.xx.xxxx:xxxx;
    }
    }
  3. 重新加载Nginx配置文件

    执行以下命令重新加载配置文件

    1
    systemctl reload nginx
  4. 访问项目

    访问http://xxxxxx