# 项目整体功能概览

image-20240606224857165

定位:专门为餐饮企业(餐厅、饭店)定制的一款软件产品

# 项目开发流程

image-20240606224959138

# 角色分工

image-20240606225037927

# 功能架构

image-20240606225200139

# 产品原型

产品原型:用于展示项目的业务功能,一般由产品经理进行设计

image-20240606225721209

进入文件夹后直接打开 html 文件即可查看原型内容,“登录” html 为入口,这里不做过多展示,不是我们的重点方向。

# 技术选型

技术选型:展示项目中使用到的技术框架和中间件等

image-20240606225938030

# 开发环境搭建

# 整体结构

image-20240606230044718

# 前端环境搭建

image-20240606230130767

这里项目前端代码和 nginx 已经构建完毕,我们直接运行 nginx 即可。

[!NOTE]

  • Nginx 目录必须放在没有中文的目录中才能正常运行!!!
  • 当前 Nginx 的配置文件中已经配置了反向代理,通过此配置可以将前端请求转发到后端服务

运行后,浏览器输入 localhost:80

image-20240606230417929

前端运行无误

# 后端环境搭建

image-20240606230452930

# 熟悉项目结构

序号名称说明
1sky-take-outmaven 父工程,统一管理依赖版本,聚合其他子模块
2sky-common子模块,存放公共类,例如:工具类、常量类、异常类等
3sky-pojo子模块,存放实体类、VO、DTO 等
4sky-server子模块,后端服务,存放配置文件、Controller、Service、Mapper 等

sky-common 子模块中存放的是一些公共类,可以供其他模块使用

image-20240607000138282

sky-pojo 子模块中存放的是一些 entity、DTO、VO

image-20240607000241076

名称说明
Entity实体,通常和数据库中的表对应
DTO数据传输对象,通常用于程序中各层之间传递数据
VO视图对象,为前端展示数据提供的对象
POJO普通 Java 对象,只有属性和对应的 getter 和 setter

sky-server 子模块中存放的是 配置文件、配置类、拦截器、controller、service、mapper、启动类等

image-20240607000449067

# 使用 Git 进行版本控制

这个基本操作了,不做过多介绍,可以自行搜索

# 数据库环境搭建

导入项目 sky.sql,(博主用的是 navicat)

image-20240606231317014

在后端项目的 sky-server/src/main/resources/application-dev.yml 文件里进行数据库信息配置

1
2
3
4
5
6
7
8
9
sky:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
host: localhost
port: 3306
database: sky_take_out
username: root
password: root

# 前后端联调

后端的初始工程中已经实现了登录功能,直接进行前后端联调测试即可

image-20240606231508371

[!NOTE]

可以通过断点调试跟踪后端程序的执行过程

# 思考:前端发送的请求,是如何请求到后端服务的?

前端请求地址:http://localhost/api/employee/login

image-20240607000922299

后端接口地址:http://localhost:8080/admin/employee/login

image-20240607001005021

解答 :这是利用 nginx 反向代理,就是将前端发送的动态请求由 nginx 转发到后端服务器

image-20240606231756869

nginx 反向代理的好处:

  • 提高访问速度
  • 进行负载均衡
  • 保证后端服务安全

所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器

image-20240606231901928

nginx 反向代理的配置方式:

1
2
3
4
5
6
# 反向代理,处理管理端发送的请求
location /api/ {
proxy_pass http://localhost:8080/admin/;
#proxy_pass http://webservers/admin/;
}

相当于一旦访问 localhost/api/ 这个路径,直接定向到 http://localhost:8080/admin/ 这个路径资源下

nginx 负载均衡的配置方式:

这是默认的负载均衡算法,Nginx 按照请求的顺序依次将请求分配给后端的服务器。每个服务器按照其权重来处理请求,然后按顺序循环分配。这种算法简单且平均地将负载分配给后端服务器,适用于后端服务器配置相同、处理能力相当的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

http {
upstream backend {
server 192.168.1.101:8080;
server 192.168.1.102:8080;
server 192.168.1.103:8080;
}

server {
listen 80;

location / {
proxy_pass http://backend;
}
}
}

在 upstream 下配置多台后端主机的 ip 地址和端口,在后面的 proxy_pass 配置代理路径地址

nginx 负载均衡策略:

名称说明
轮询默认方式
weight权重方式,默认为 1,权重越高,被分配的客户端请求就越多
ip_hash依据 ip 分配方式,这样每个访客可以固定访问一个后端服务
least_conn依据最少连接方式,把请求优先分配给连接数少的后端服务
url_hash依据 url 分配方式,这样相同的 url 会被分配到同一个后端服务
fair依据响应时间方式,响应时间短的服务将会被优先分配

[!TIP]

【Nginx】Nginx 配置文件解读和 4 种常用实现负载均衡的方式_nginx 配置负载均衡 - CSDN 博客

# 管理员登录功能代码(基础版)

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
48
49
50
51
@RestController
@RequestMapping("/admin/employee")
@Slf4j
public class EmployeeController {

@Autowired
private EmployeeService employeeService;
@Autowired
private JwtProperties jwtProperties;

/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);

Employee employee = employeeService.login(employeeLoginDTO);

//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);

EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();

return Result.success(employeeLoginVO);
}

/**
* 退出
*
* @return
*/
@PostMapping("/logout")
public Result<String> logout() {
return Result.success();
}

}

EmployeeLoginDTO, dto 作为前端传过来的数据对象,被后端接收后,会通过调用 employeeService.login 来验证账号密码是否正确,并返回一个 employee 对象,具体实现逻辑是,employeeService 这个接口会锁定它的具体实现类中的 login 方法,如下是具体的实现类:

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
public class EmployeeServiceImpl implements EmployeeService {

@Autowired
private EmployeeMapper employeeMapper;

/**
* 员工登录
*
* @param employeeLoginDTO
* @return
*/
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();

//1、根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);

//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (employee == null) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}

//密码比对
// TODO 后期需要进行md5加密,然后再进行比对
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}

if (employee.getStatus() == StatusConstant.DISABLE) {
//账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}

//3、返回实体对象
return employee;
}

}

在该实现类中,会调用 employeeMapper 接口的方法来根据用户名查询用户是否存在并返回 employee 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
@Mapper
public interface EmployeeMapper {

/**
* 根据用户名查询员工
* @param username
* @return
*/
@Select("select * from employee where username = #{username}")
Employee getByUsername(String username);

}

当验证成功后,会返回 employee 对象,同时,实现类也会成功接收 employee 对象,并继续执行下一步:生成 jwt 令牌,是通过 JwtUtil.createJWT 该工具类下的该方法进行实现。

其中 JwtProperties 在 sky-common 下的 src/main/java/com/sky/properties/JwtProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;

/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;

}

我们可以看到 @ConfigurationProperties (prefix = "sky.jwt") 会自动锁定在 application-dev.yml 下的配置文件中

1
2
3
4
5
6
7
8
9
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token

生成 jwt 令牌后,通过 builder 方法,生成 VO 对象传给前端。

1
2
3
4
5
6
7
8
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();

return Result.success(employeeLoginVO);

以下是几个可能会考的面试问题:

[!NOTE]

Employee employee = employeeService.login (employeeLoginDTO); 为什么要用 employeeService 接口调用 login 方法,而不是它的实现类来调用 login 方法

  1. 解耦合:通过服务接口进行调用,可以将调用者和具体实现类的耦合度降低。调用者只需要依赖接口定义的方法签名,而不需要关心具体实现类的细节。这有利于代码的可维护性和扩展性。
  2. 灵活性:如果将来需要替换或修改登录功能的实现,只需要修改服务接口的实现类,而调用者的代码不需要修改。这提高了系统的灵活性。
  3. 多态性:服务接口可以有多个不同的实现类,调用者可以根据需要选择不同的实现。这增加了系统的可扩展性。
  4. 测试性:在测试时,可以使用模拟 / 桩实现替换真实的服务实现,这样可以隔离测试目标,提高测试的可靠性。
  5. IoC/DI: 服务接口配合依赖注入 (DI) 机制,可以更好地实现控制反转 (IoC) 的设计模式,增强系统的可测试性和可维护性。

[!NOTE]

为什么接口可以直接调用实现类中的方法

  1. 多态性:在面向对象编程中,接口可以作为一种数据类型,实现类可以被视为接口类型的实例。当通过接口引用调用方法时,实际执行的是实现类中对应的方法实现。这就是多态性的体现。
  2. 动态绑定:在编译时,编译器只知道变量的声明类型 (接口), 但在运行时才能确定变量的实际类型 (实现类)。方法的动态绑定机制会在运行时根据实际对象的类型来决定调用哪个方法实现。
  3. 依赖倒转原则:这是面向对象设计的一个重要原则。它要求高层模块 (调用者) 应该依赖于抽象接口,而不应该依赖于具体实现。这样可以提高代码的灵活性和可维护性。
  4. 接口隔离原则:接口应该尽可能小而专,只提供客户端所需的方法。这样可以降低实现类的耦合度,隔离客户端与实现类之间的依赖。
  5. 依赖注入:现代框架如 Spring 都广泛采用依赖注入的设计模式。通过接口引用调用方法,可以很好地支持依赖注入,增强系统的可测试性和可扩展性。

# 完善登录功能

问题:员工表中的密码是明文存储,安全性太低

image-20240607002130508

思路:

  1. 将密码加密后存储,提高安全性

  2. 使用 MD5 加密方式对明文密码加密

  3. 修改数据库中明文密码,改为 MD5 加密后的密文

  4. 修改 Java 代码,前端提交的密码进行 MD5 加密后再跟数据库中密码比对

1
2
3
4
5
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}

[!NOTE]

password = DigestUtils.md5DigestAsHex(password.getBytes());

这是 DigestUtils 工具类下的方法,可以直接用。

# 导入接口文档

前后端分离开发流程:

image-20240606232723991

接口的开发不是一蹴而就的,在整个前后端联调过程中,都有可能临时发现接口的异常或其他问题,都需要进行修改。

导入接口文档需要用到 YApi

image-20240606232956418

导入 json 文件到 YApi,便可查看接口文档。

# Swagger

接口文档的编写是一个协作的过程,需要软件架构师、设计师、开发和测试人员通力合作。他们各司其职,共同确保接口文档的质量。

在后端开发环境中,使用 Swagger 你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。

[!IMPORTANT]

Knife4j 是为 Java MVC 框架集成 Swagger 生成 Api 文档的增强解决方案。

# 使用方式

  1. 导入 knife4j 的 maven 坐标

    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.2</version>
    </dependency>

  2. 在配置类中加入 knife4j 相关配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Bean
    public Docket docket() {
    ApiInfo apiInfo = new ApiInfoBuilder()
    .title("苍穹外卖项目接口文档")
    .version("2.0")
    .description("苍穹外卖项目接口文档")
    .build();
    Docket docket = new Docket(DocumentationType.SWAGGER_2)
    .apiInfo(apiInfo)
    .select()
    .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
    .paths(PathSelectors.any())
    .build();
    return docket;
    }

  3. 设置静态资源映射,否则接口文档页面无法访问

    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * 设置静态资源映射
    * @param registry
    */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
    registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    [!IMPORTANT]

    2,3 步 的内容放在了 sky-server 下的 config 的 WebMvcConfiguration 类下

    配置好后,项目启动后,接口文档的访问路径在 http://ip:port/doc.html,本地搭建就是在 http://127.0.0.1:8080/doc.html 下,如下图所示,可以查看文档接口信息并进行接口测试。

    image-20240606235414274

# 通过 Swagger 就可以生成接口文档,那么我们就不需要 Yapi 了?

  1. Yapi 是设计阶段使用的工具,管理和维护接口
  2. Swagger 在开发阶段使用的框架,帮助后端开发人员做后端的接口测试

# 常用注解

通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:

注解说明
@Api用在类上,例如 Controller,表示对类的说明
@ApiModel用在类上,例如 entity、DTO、VO
@ApiModelProperty用在属性上,描述属性信息
@ApiOperation用在方法上,例如 Controller 的方法,说明方法的用途、作用