这是一个使用Springboot+SSM的实战练习项目,实现了一个外卖系统的前后台功能。
本笔记源自黑马程序员的视频课程——《瑞吉外卖》,总结了课程笔记、相关知识点以及可能遇到的问题解决方案,并且增加了课程中未实现的功能,供读者参考。笔记全面且条理清晰,希望帮助读者学习和理解这个外卖项目。
本项目全部笔记见:外卖项目笔记合集
下图是项目效果展示,左侧是移动端的效果,右侧是管理后台的效果。

1. 软件开发整体介绍
1.1 软件开发流程

1.2 角色分工

1.3 软件环境

2. 外卖项目介绍
2.1 项目介绍
本外卖项目是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。
本项目共分为3期进行开发:
第一期主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问。
第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便。
第三期主要针对系统进行优化升级,提高系统的访问性能。
2.2 产品原型展示
产品原型,就是一款产品成型之前的一个简单的框架,就是将页面的排版布局展现出来,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观的了解项目的需求和提供的功能。
一般来说,产品原型由产品经理在“需求分析”阶段制作。

例如:

注意事项:产品原型主要用于展示项目的功能,并不是最终的页面效果。
2.3 技术选型

2.4 功能架构

2.5 角色
后台系统管理员:登录后台管理系统,拥有后台系统中的所有操作权限。
后台系统普通员工:登录后台管理系统,对菜品、套餐、订单等进行管理。
C端用户:登录移动端应用,可以浏览菜品、添加购物车、设置地址、在线下单等。
3. 开发环境搭建
3.1 数据库环境搭建
3.1.1 创建项目对应的数据库
图形界面或者命令行方式任选一个
方法一:使用图形界面创建数据库(navicat等)

方法二:使用命令行创建数据库

创建后在图形界面内刷新一下,就可以看到新创建的数据库了。
3.1.2 导入表结构
在资料/数据模型/db_reggie.sql有脚本文件。
在图形界面中,选中数据库点右键,选择“运行SQL文件”,文件选择资料中提供的db_reggie.sql文件,点击开始。刷新后可以看到所有的表。

3.1.3 数据表

3.2 maven项目搭建
3.2.1 创建maven项目
在IDEA中点击new,选择project,选择maven项目。

3.2.2 检查项目的编码、maven仓库配置、jdk配置
创建完项目后,注意检查项目的编码、maven仓库配置、jdk配置等。
项目编码:

maven仓库配置:

jdk配置:

3.2.3 pom文件
pom文件中添加依赖:
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.8</version> <relativePath/> </parent> <groupId>com.kxzhu</groupId> <artifactId>reggie</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.23</version> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build>
|
3.2.4 配置文件application.yml
将资料/项目文件中提供的application.yml复制到项目的src/main/resources下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| server: port: 8080
spring: application: name: reggie datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: root password: root
mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: ASSIGN_ID
|
3.2.5 编写Springboot启动类
1 2 3 4 5 6 7 8
| @Slf4j @SpringBootApplication public class ReggieApplication { public static void main(String[] args) { SpringApplication.run(ReggieApplication.class, args); log.info("项目启动成功"); } }
|
注解@Slf4j是lombok自带的。加在类上之后,类中的方法可以调用log.info()方法。
3.2.6 导入前端页面并设置静态资源映射
导入项目前端页面
将资料/前端资源中的backend复制,粘贴到项目的src/main/resources。
注意:资料/前端资源/front部分代码不全,在后期需要手动修改。为了简便,此处可以复制资料中day05的front资源。
设置静态资源映射
原因:
此时存在一个问题,springboot默认情况下只能访问以下静态资源路径:
- classpath:/static/
- classpath:/templates/
- classpath:/META-INF/resources/
- classpath:/resources/
此处的类路径“classpath”相当于src/main/resources目录。现在我们想将静态资源放在resources下的backend和front目录,并能访问,需要设置MVC框架静态资源映射。
方法:
在与spring boot启动类(Springboot2Application)的同级文件下,创建config包,并创建配置类WebMvcConfig,继承WebMvcConfigurationSupport类,并重写addResourceHandlers(ResourceHandlerRegistry registry)方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Slf4j @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/"); registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/"); log.info("开始进行静态资源映射"); } }
|
拓展知识:
/** 当前目录及子目录下的所有文件
/* 当前目录下的所有文件
4. 后台登录功能开发
4.1 需求分析
由于已经将静态资源放到resources下,且已经设置了静态资源映射,因此可以访问resources下的静态资源。此时可以直接在浏览器访问登录页面 http://localhost:8080/backend/page/login/login.html 。并可以打开浏览器调试工具(按F12;或者在浏览器点右键,选择“检查”),切换到“Network”。如下图:

4.1.1 请求过程
在登录页面 http://localhost:8080/backend/page/login/login.html 点击“登录”按钮,可以看到向http://localhost:8080/employee/login 发送了POST请求,并将username和password以json字符串的形式提交到服务端。

服务端需要写相关的方法处理该POST请求。
4.1.2 数据模型
由于此时是员工登录,所以相关的数据表是employee表。

4.2 代码开发
4.2.1 前端页面分析
我们首先需要先大致读懂html文件,可以分析出,服务端处理数据后,需要给前端什么样的响应信息。
在resources/backend/page/login/login.html中,关键代码是methods方法。

点击”登录按钮”时,执行methods中的handleLogin
方法。其中先校验用户名和密码是否为空。
如果登录通过,loading改为true,页面显示”登录中”。并调用loginApi()
,向/employee/login发送post请求,并且带上loginForm数据,即json形式的username和password。请求发出后,服务端接收、处理并返回相应结果,res
就是controller响应回来的结果。响应回来的code为1,则表示成功,跳转到首页。
否则(登录失败)loading为false,页面显示”登录”。

注:从前端代码可以看出,服务端处理完,给页面响应回来的数据应该含有code, data, msg。且localStorage.setItem('userInfo',JSON.stringify(res.data))
这句说明,需要将’userInfo’作为键,将响应回来的data转为json字符串,作为值,保存在浏览器中。因此,data应该包含登录成功的员工信息。
4.2.2 实体类
创建实体类Employee,和employee表进行映射:
创建entity包,将资料提供的实体类Employee类复制粘贴到entity包中。
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
| @Data public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
private String idNumber;
private Integer status;
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT) private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE) private Long updateUser;
}
|
4.2.3 创建Controller、Service、Mapper
EmployeeMapper:
在mapper包下,创建EmployeeMapper接口。并记得加上@Mapper注解。
因为这里使用mybatis-plus,所以可以直接继承其提供的BaseMapper类,较方便。
1 2 3
| @Mapper public interface EmployeeMapper extends BaseMapper<Employee> { }
|
注:BaseMapper
是mybatis plus提供的,可以继承其增删改查方法。BaseMapper需要提供一个泛型,是实体类。
EmployeeService:
在service包下,创建EmployeeService接口。
1 2
| public interface EmployeeService extends IService<Employee> { }
|
注:IService
是mybatis-plus提供的,需要提供一个泛型,是实体类。
EmployeeServiceImpl:
在service/impl包下,创建EmployeeServiceImpl类。并记得加上@Service注解。
1 2 3
| @Service public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService{ }
|
EmployeeController:
在controller包下,创建EmployeeController类。加上@RestController、@RequestMapping(“/employee”)、@Slf4j注解。
1 2 3 4 5 6 7
| @Slf4j @RestController @RequestMapping("/employee") public class EmployeeController { @Autowired private EmployeeService employeeService; }
|
4.2.4 导入返回结果类R
R是一个通用结果类,服务端响应的所有结果最终都会包装成此种类型,返回给前端页面。
具体地,服务端controller响应客户端页面发来的请求,controller处理完之后给页面返回结果,把结果封装到此R类对象中。
将资料/服务端返回结果类中的R.java粘贴到common包中。
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
| @Data public class R<T> {
private Integer code;
private String msg;
private T data;
private Map map = new HashMap();
public static <T> R<T> success(T object) { R<T> r = new R<T>(); r.data = object; r.code = 1; return r; }
public static <T> R<T> error(String msg) { R r = new R(); r.msg = msg; r.code = 0; return r; }
public R<T> add(String key, Object value) { this.map.put(key, value); return this; } }
|
4.2.5 在Controller中编写登录方法

处理逻辑如下:
- 将页面提交的密码password进行md5加密处理
- 根据页面提交的用户名username查询数据库
- 如果没有查询到则返回登录失败结果
- 密码比对,如果不一致则返回登录失败结果
- 查看员工状态,如果为已禁用状态,则返回员工已禁用结果
- 登录成功,将员工id存入Session并返回登录成功结果
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
| @Slf4j @RestController @RequestMapping("/employee") public class EmployeeController { @Autowired private EmployeeService employeeService;
@PostMapping("/login") public R<Employee> login(@RequestBody Employee employee, HttpServletRequest request){ String password = employee.getPassword(); password = DigestUtils.md5DigestAsHex(password.getBytes());
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Employee::getUsername, employee.getUsername()); Employee emp = employeeService.getOne(queryWrapper);
if(emp == null){ return R.error("登录失败"); }
if(! emp.getPassword().equals(password)){ return R.error("登录失败"); }
if(emp.getStatus() == 0){ return R.error("账号已禁用"); }
HttpSession session = request.getSession(); session.setAttribute("employee", emp.getId()); return R.success(emp);
|
拓展:
第2步中的LambdaQueryWrapper是mybatis-plus的一个工具类。如果你已经学过mybatis-plus,可以忽略这一段。如果没学过,可以看这段介绍简单了解用法。
LambdaQueryWrapper常见功能:

LambdaQueryWrapper最基础的使用方式是这样:
1 2 3 4 5 6 7 8 9 10 11 12
| QueryWrapper<BannerItem> wrapper = new QueryWrapper<>(); wrapper.eq("banner_id", id);
List<BannerItem> bannerItems = bannerItemMapper.selectList(wrapper);
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername, employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);
|
其中,queryWrapper.eq(Employee::getUsername, employee.getUsername());
这一句用来判断:在数据库中遍历出的用户名,是否和前端页面用户输入的用户名(封装在employee中)相同。相同的话,代表数据库中有这个值,反之则无这个值。
对应的sql语句:select * from employee where username = ?
4.3 功能测试
启动服务,进行登录功能的测试。
报错:timeout of 10000ms exceeded

前端页面设置了超时时间,10秒不响应就会抛异常。测试阶段可以将backend/js/request.js中的超时时间改大,改为1000000

5. 后台退出功能开发
5.1 需求分析
员工登录成功后,页面跳转到后台系统首页面(backend/index.html),此时会显示当前登录用户的姓名。

如果员工需要退出系统,点击右侧的退出按钮。打开浏览器调试工具,可以看到向http://localhost:8080/employee/logout 发送了POST请求,此处需要服务端处理。

退出系统后页面跳转回登录页面。
5.2 代码开发
服务端controller需要写相关的类处理该POST请求(/employee/logout)。具体的处理逻辑:
- 清理Session中的用户id
- 返回结果
1 2 3 4 5 6 7 8 9 10 11 12
|
@PostMapping("/logout") public R<String> logout(HttpServletRequest request){ HttpSession session = request.getSession(); session.removeAttribute("employee"); return R.success("退出成功"); }
|
由于此处不需要返回详细数据,返回类型R即可。
5.3 功能测试
启动服务,测试功能即可。