云计算百科
云计算领域专业知识百科平台

苍穹外卖

项目整体展示:

开发环境搭建:

前端环境搭建:

前端工程基于 nginx:

启动 nginx 服务访问测试:

后端环境搭建:

项目结构:

后端工程基于 maven 进行项目构建,并且进行分模块开发

工程的各个模块作用说明:

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

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

sky-common 模块下每个包的作用:

名称说明
constant 存放相关常量类
context 存放上下文类
enumeration 项目的枚举类存储
exception 存放自定义异常类
json 处理 json 转换的类
properties 存放 SpringBoot 相关的配置属性类
result 返回结果类的封装
utils 常用工具类

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

sky-pojo 模块下每个包的作用:

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

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

sky-server 模块下每个包的作用:

名称说明
config 存放配置类
controller 存放controller类
interceptor 存放拦截器类
mapper 存放mapper接口
service 存放service类
SkyApplication 启动类

Git 版本控制:

使用 Git 进行项目代码的版本控制,具体操作:

创建 Git 本地仓库:

创建 Git 远程仓库:

访问 https://gitee.com/,新建仓库

将本地文件推送到 Git 远程仓库:

提交文件至本地仓库

添加 Git 远程仓库地址

推送至远程仓库

数据库搭建:

在 MySQL Workbench 中执行 sky.sql 文件创建数据库

每张表的说明:

表名中文名
employee 员工表
category 分类表
dish 菜品表
dish_flavor 菜品口味表
setmeal 套餐表
setmeal_dish 套餐菜品关系表
user 用户表
address_book 地址表
shopping_cart 购物车表
orders 订单表
order_detail 订单明细表

前后端联调:

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

实现思路:

Controller 层:

EmployeeController:

/**
* 登录
*
* @param employeeLoginDTO 登录请求参数
* @return 登录结果(包含JWT令牌和员工信息)
*/
@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
);
//封装登录返回VO
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}

Service 层:

EmployeeServiceImpl:

/**
* 员工登录校验
*
* @param employeeLoginDTO 员工登录请求参数(包含用户名、密码)
* @return 登录成功的员工实体
* @throws AccountNotFoundException 账号不存在异常
* @throws PasswordErrorException 密码错误异常
* @throws AccountLockedException 账号被锁定异常
*/
public Employee login(EmployeeLoginDTO employeeLoginDTO){
//获取登录请求中的用户名和密码
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();
//根据用户名查询数据库中的员工信息
Employee employee = employeeMapper.getByUsername(username);
//校验账号有效性:用户名不存在
if(employee == null){
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
//校验密码正确性(TODO:后续需对密码做MD5加密后再比对)
if(!password.equals(employee.getPassword())){
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
//校验账号状态:是否被锁定
if(employee.getStatus() == StatusConstant.DISABLE){
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
//所有校验通过,返回员工实体
return employee;
}

Mapper 层:

EmployeeMapper:

/**
* 根据用户名查询员工信息
*
* @param username 员工用户名(唯一标识)
* @return 匹配的员工实体,无匹配数据时返回null
*/
@Select("select * from employee where username = #{username}")
Employee getByUsername(String username);

Nginx:

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

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

反向代理:

Nginx 反向代理,是指客户端请求不直接访问后端服务器,而是先发送到 Nginx 服务器,由 Nginx 代替客户端向后端服务器发起请求,再将后端的响应结果返回给客户端的一种网络架构模式,可以提高访问速度,进行负载均衡,保证后端服务安全

配置方式:

server{
listen 80;
server_name localhost;

location /api/{
proxy_pass http://localhost:8080/admin/; #反向代理
}
}

含义:监听 80 端口号,当访问 http://localhost:80/api/../.. 这样的接口时,它会通过 location /api/ {} 这样的反向代理到 http://localhost:8080/admin/ 上

proxy_pass:该指令是用来设置代理服务器的地址,可以是主机名称,IP 地址加端口号等形式

配置文件 nginx.conf :

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

访问 http://localhost/api/employee/login 时,nginx 接收请求后转到 http://localhost:8080/admin/,故最终的请求地址为 http://localhost:8080/admin/employee/login,和后台服务的访问地址一致

负载均衡:

Nginx 负载均衡,是指通过 Nginx 作为反向代理服务器,将客户端的请求均匀地分发到后端多台应用服务器,避免单台服务器因请求过载宕机,同时提升系统的并发处理能力、可用性和容灾能力

配置方式:

upstream webservers{
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
server{
listen 80;
server_name localhost;

location /api/{
proxy_pass http://webservers/admin;#负载均衡
}
}

含义: 监听 80 端口号,当访问 http://localhost:80/api/../.. 这样的接口时,它会通过 location /api/ {} 这样的反向代理到 http://webservers/admin,通过 webservers 标识的后端服务器集群,依据预设负载均衡策略(默认轮询),将客户端请求分发至集群内的具体服务器

负载均衡策略:

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

具体配置方式:

轮询:

upstream webservers{
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}

weight:

upstream webservers{
server 192.168.100.128:8080 weight=90;
server 192.168.100.129:8080 weight=10;
}

ip_hash:

upstream webservers{
ip_hash;
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}

least_conn:

upstream webservers{
least_conn;
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}

url_hash:

upstream webservers{
hash &request_uri;
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}

fair:

upstream webservers{
server 192.168.100.128:8080;
server 192.168.100.129:8080;
fair;
}

完善登录功能:

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

解决思路:使用 MD5 加密方式对明文密码加密后存储

代码实现:

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

EmployeeServiceImpl:

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

导入接口文档:

前后端分离:

开发流程:定义接口,确定接口的路径、请求方式、传入参数、返回参数;前端开发人员和后端开发人员并行开发,同时也可自测;前后端人员进行连调测试;提交给测试人员进行最终测试

操作步骤:

Swagger:

介绍:

官网:https://swagger.io/

Swagger 是一套标准化框架,用于 RESTful Web 服务的接口生成、描述、调用和可视化,核心价值是降低接口文档编写成本、简化前后端协作,还支持接口功能测试;Spring 基于 Swagger 封装了 Springfox 项目,便于 Java 项目快速集成

Knife4j 是适配 Java MVC 框架的 Swagger 增强方案(前身是 swagger-bootstrap-ui),以轻量、功能强为特点,是目前 Java 项目中集成 Swagger 的主流选择

使用步骤:

导入 knife4j 的 maven 坐标:

<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>

在 WebMvcConfiguration 配置类中加入 knife4j 相关配置:

/**
* 配置Knife4j生成接口文档
* @return Docket Swagger核心配置对象
*/
@Bean
public Docket docket(){
log.info("准备生成接口文档…");
//构建接口文档的基础信息(标题、版本、描述)
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();

//构建Swagger/Docket核心配置
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo) //绑定接口文档基础信息
.select() //开启接口扫描配置
//扫描指定包下的所有控制器(核心业务接口)
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())//匹配所有路径的接口
.build();

return docket;
}

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

/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry){
log.info("开始设置静态资源映射…");
//映射Knife4j接口文档首页(doc.html)的静态资源
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
//映射Knife4j依赖的webjars静态资源(如CSS/JS/字体等)
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}

访问测试:

接口测试:测试登录功能

常用注解:

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

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

EmployeeLoginDTO:

@Data
@ApiModel(description = "员工登录时传递的数据模型")
public class EmployeeLoginDTO implements Serializable{

@ApiModelProperty("用户名")
private String username;

@ApiModelProperty("密码")
private String password;

}

EmployeeLoginVo:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable{

@ApiModelProperty("主键值")
private Long id;

@ApiModelProperty("用户名")
private String userName;

@ApiModelProperty("姓名")
private String name;

@ApiModelProperty("jwt令牌")
private String token;

}

EmployeeController:

/**
* 员工管理
*/
@RestController
@RequestMapping("/admin/employee")
@Slf4j
@Api(tags = "员工相关接口")
public class EmployeeController{

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

/**
* 登录
*
* @param employeeLoginDTO 登录请求参数
* @return 登录结果(包含JWT令牌和员工信息)
*/
@PostMapping("/login")
@ApiOperation(value = "员工登录")
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
);
//封装登录返回VO
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}

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

}

启动服务:访问 http://localhost:8080/doc.html

新增员工:

需求分析和设计:

产品原型:

新增员工原型:

接口设计:

项目约定:管理端发出的请求,统一使用 /admin 作为前缀;用户端发出的请求,统一使用 /user作为前缀

表设计:

employee 表结构:

字段名数据类型说明备注
id bigint 主键 自增
name varchar(32) 姓名
username varchar(32) 用户名 唯一
password varchar(64) 密码
phone varchar(11) 手机号
sex varchar(2) 性别
id_number varchar(18) 身份证号
status Int 账号状态 1 正常 0 锁定
create_time Datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id

表中的 status 字段已设置默认值 1 表示状态正常

代码开发:

设计 DTO 类:

EmployeeDTO:

@Data
public class EmployeeDTO implements Serializable{

private Long id;

private String username;

private String name;

private String phone;

private String sex;

private String idNumber;

}

Controller 层:

EmployeeController:

/**
* 新增员工
* @param employeeDTO
* @return
*/
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO){
log.info("新增员工:{}", employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}

Result 类定义了后端统一返回结果格式:

/**
* 后端统一返回结果
* @param <T>
*/
@Data
public class Result<T> implements Serializable{

private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据

public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}

public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}

public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}

}

Service 层:

EmployeeService:

/**
* 新增员工
* @param employeeDTO
*/
void save(EmployeeDTO employeeDTO);

EmployeeServiceImpl:

/**
* 新增员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO){
Employee employee = new Employee();
//对象属性拷贝
BeanUtils.copyProperties(employeeDTO,employee);
//设置账号的状态,默认正常状态 1 正常 0 锁定
employee.setStatus(StatusConstant.ENABLE);
//设置密码,默认密码123456
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//设置当前记录创建人id和修改人id
employee.setCreateUser(10L);//假数据
employee.setUpdateUser(10L);

employeeMapper.insert(employee);
}

StatusConstant:

/**
* 状态常量,启用或者禁用
*/
public class StatusConstant{

//启用
public static final Integer ENABLE = 1;

//禁用
public static final Integer DISABLE = 0;
}

Mapper 层:

EmployeeMapper:

/**
* 插入员工数据
* @param employee
*/
@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +
"values " +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
void insert(Employee employee);

在 application.yml 中已开启驼峰命名,故 id_number 和 idNumber 可对应

mybatis:
configuration:
#开启驼峰命名
map-underscore-to-camel-case: true

功能测试:

接口文档测试:

启动服务:访问 http://localhost:8080/doc.html,进入新增员工接口

响应码:401 报错;原因:JWT 令牌校验失败

解决方法:调用员工登录接口获得一个合法的 JWT 令牌

将合法的 JWT 令牌添加到全局参数中

接口测试:

查看 employee 表:

前后端联调测试:

访问 http://localhost

查看 employee 表:

代码完善:

问题一:

录入的用户名已存,抛出的异常后没有处理

GlobalExceptionHandler:

/**
* 处理SQL异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}

在 MessageConstant 中添加

public static final String ALREADY_EXISTS = "已存在";

接口测试:

问题二:

新增员工时,创建人 id 和修改人 id 设置为固定值

employee.setCreateUser(10L);
employee.setUpdateUser(10L);

解决方法:员工登录成功后生成 JWT 令牌并返回至前端;前端后续发起请求时携带该令牌,后端解析令牌获取当前登录员工 ID,通过 ThreadLocal 将该 ID 传递至 Service 层的 save 方法中使用

ThreadLocal:

介绍:ThreadLocal 并不是一个Thread,而是Thread的局部变量;ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问

常用方法:

public void set(T value)        设置当前线程的线程局部变量的值

public T get()        返回当前线程所对应的线程局部变量的值

public void remove()        移除当前线程的线程局部变量

初始工程中已经封装了 ThreadLocal 操作的工具类:

public class BaseContext{

public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

public static void setCurrentId(Long id){
threadLocal.set(id);
}

public static Long getCurrentId(){
return threadLocal.get();
}

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

}

在拦截器中解析出当前登录员工 id,并放入线程局部变量中:

/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor{

@Autowired
private JwtProperties jwtProperties;

/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
//判断当前拦截到的是Controller的方法还是其他资源
if(!(handler instanceof HandlerMethod)){
//当前拦截到的不是动态方法,直接放行
return true;
}

//从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());

//校验令牌
try{
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
//将用户id存储到ThreadLocal
BaseContext.setCurrentId(empId);
//通过,放行
return true;
}catch (Exception ex){
//不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}

在 Service 中获取线程局部变量中的值:

/**
* 新增员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO){
Employee employee = new Employee();
//对象属性拷贝
BeanUtils.copyProperties(employeeDTO,employee);
//设置账号的状态,默认正常状态 1 正常 0 锁定
employee.setStatus(StatusConstant.ENABLE);
//设置密码,默认密码123456
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//设置当前记录创建人id和修改人id
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());

employeeMapper.insert(employee);
}

测试:使用 admin(id=1)用户登录后添加一条记录

查看 employee 表:

代码提交:

赞(0)
未经允许不得转载:网硕互联帮助中心 » 苍穹外卖
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!