一、瑞吉外卖项目介绍
1、项目背景介绍
本项目(瑞吉外卖
)是专门为餐饮企业(餐厅、饭店
)定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护
。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等
。
本项目供分为3期进行开发
第一期
实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问;
第二期
针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便;
第三期
针对系统进行优化升级,提高系统的访问性能;
2、产品原型介绍
产品原型
一款产品成型之前的一个简单框架,就是将页面的排版布局展现出现,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观的了解项目的需求和提供的功能。
注意事项:产品原型主要用于展示项目的功能,并不是最终的页面效果
技术选型
3、功能架构
4、角色
后台系统管理员
- 登录后台管理系统,拥有后台系统中的所有操作权限;
后台系统普通员工
- 登录后台管理系统,对菜品、套餐订单等进行管理;
C端用户
- 登录移动端应用,可以浏览菜品、添加购物车、设置地址、在线下单等;
二、开发环境搭建
项目架构
1、数据库
1.1 创建数据库reggie
1.2 导入db_reggie.sql
并执行sql
数据表
表名 | 信息 |
---|---|
employee | 员工表 |
category | 菜品和套餐分类表 |
dish | 菜品表 |
setmeal | 套餐表 |
setmeal_dish | 套餐菜品关系表 |
dish_flavor | 菜品口味关系表 |
user | 用户表 |
address_book | 地址簿表 |
shopping_cart | 购物车表 |
orders | 订单表 |
order_detail | 订单明细表 |
2、构件Maven项目
2.1 新建Maven项目
2.2 导入jar包
druid jar包异常
解决:更换druid版本号
<!--父依赖-->
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.4.RELEASE</version>
</parent>
<dependencies>
<!--spring boot启动依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--spring boot test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--web启动依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--MP启动依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--fastJSON-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.1.23</version>
</dependency>
<!--commons-lang-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.11</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.6</version>
</plugin>
</plugins>
</build>
2.3 编写配置文件
创建application.yml
文件
# 端口号
server:
port: 8080
spring:
application:
# 应用名称,选择型配置
name: reggie_take_out
# 数据源
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&userSSL=false
username: root
password: 123456
# MP配置
mybatis-plus:
configuration:
# 数据库映射 驼峰命名 user_name -> userName
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
global-config:
db-config:
# 自动生成id
id-type: assign_id
2.4 导入静态资源
静态资源映射
直接复制粘贴在resources
路径下
设置静态资源映射
@Configuration
@Slf4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射");
registry.addResourceHandler("/backend/**")
.addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**")
.addResourceLocations("classpath:/front/");
}
}
2.5 编写启动类
@SpringBootApplication
@Slf4j
public class ReggieApplication {
public static void main(String[] args) {
log.info("项目启动成功!");
SpringApplication.run(ReggieApplication.class, args);
}
}
2.6 测试
三、后台系统开发
1、登录系统
需求分析
通过访问登录页面http://localhost:8080/backend/page/login/login.html
,点击登录按钮时,页面会发送请求login
以及提交的参数username
和password
1.1 用户登录
1、创建实体类
Employee
@Data
public class Employee implements Serializable {
// 序列化id
private static final long serialVersionUID=1L;
// 主键
private Long id;
// 姓名
private String name;
// 用户名
private String username;
// 密码
private String password;
// 手机号
private String phone;
// 性别
private String sex;
// 身份证号
private String idNumber;
// 状态:0:禁用,1:正常
private Integer status;
// 创建时间
private LocalDateTime createTime;
// 更新时间
private LocalDateTime updateTime;
// 创建人
private Long createUser;
// 修改人
private Long updateUser;
}
2、Dao层
EmployeeMapper
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
3、Service层
EmployeeService
public interface EmployeeService extends IService<Employee> {
}
EmployeeServiceImpl
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
4、导入通用结果类
由于前端页面需要后端接口返回对应的信息,所以引入R这个通用结果类;
R
/**
* 通用结果类
* @param <T>
*/
@Data
public class R<T> {
// 状态码
private Integer code;
// 错误信息
private String msg;
// 数据
private T data;
private Map map=new HashMap();
/**
* 成功时返回
* @param object
* @param <T>
* @return
*/
public static <T> R<T> success(T object){
R<T> r=new R<>();
r.data=object;
r.code=1;
return r;
}
/**
* 错误时返回
* @param msg
* @param <T>
* @return
*/
public static <T> R<T> error(String msg){
R<T> 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;
}
}
5、Controller
EmployeeController
@RestController
@Slf4j
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@RequestMapping(value = "/login",method = RequestMethod.POST)
public R<String> login(@RequestBody Employee employee){
log.info("employee->{}",employee);
return null;
}
}
编写具体实现时,我们需要测试前端数据,后端是否已经接收到;
1.在log.info("employee->{}",employee);
打上断点,运行程序;
2.输入http://localhost:8080/backend/page/login/login.html
进入登录界面点击登录,页面跳转到登录界面,如下所示。
3.前端登录的账号密码数据已经接收到,可以继续晚上登录方法。
处理逻辑如下
1、将页面提交的密码password进行MD5加密处理
2、根据页面提交的用户名username查询数据库
3、如果没有查询则返回登陆失败结果
4、密码对比,如果不一致则返回登录失败结果
5、查看员工状态,如果已禁用,则返回员工已禁用结果
6、登录成功,将员工id存入session并返回登陆成功结果
EmployeeController
@RestController
@Slf4j
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@RequestMapping(value = "/login",method = RequestMethod.POST)
public R<Employee> login(@RequestBody Employee employee, HttpSession session){
log.info("employee->{}",employee);
// 1.获取页面传递的密码并加密处理
String password = employee.getPassword();
password= DigestUtils.md5DigestAsHex(password.getBytes());
// 2.根据页面提交的username查询数据
// 2.1 创建条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
// 2.2 查询条件
queryWrapper.eq(Employee::getUsername,employee.getUsername());
// 2.3 查询结果
Employee emp = employeeService.getOne(queryWrapper);
// 3.如果没有查到就返回登陆失败
if (emp==null){
return R.error("登陆失败");
}
// 4.密码对比
if (!emp.getPassword().equals(password)){
return R.error("登陆失败");
}
// 5.查看员工状态是否可以直接登录 0:禁用 1:正常
if (emp.getStatus() == 0) {
return R.error("员工已禁用");
}
// 6.登陆成功,将员工id存入session
session.setAttribute("employee",emp.getId());
return R.success(emp);
}
}
1.2 用户退出
员工登陆成功后,页面跳转到后台系统首页面index.html
,此时会显示当前用户名,如果员工需要退出系统,直接点击右侧的退出按钮即可退出系统,退出系统后页面应转回登陆页面。
点击后发送logout
请求
代码实现
只需要将当前session里的员工Id清除掉即可,清除后,自动返回index.html
页面
/**
* 用户退出
* @return
*/
@RequestMapping(value = "/logout",method = RequestMethod.POST)
public R<String> logout(HttpServletRequest request){
log.info("进入退出功能");
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
1.3 登录功能完善
问题分析
前面的登录功能虽然已经开发完成,但还存在一个问题:
- 用户不登陆也可以直接访问系统首页面
这种设计并不合理,我们希望看到的效果:
- 只有登录成功后才可以访问系统中的页面,如果没有登录,则跳转到登录页面;
解决方式:拦截器
代码实现
1、创建自定义过滤器
2、在启动类上加入注解@ServletComponentScan
3、完善过滤器的处理逻辑
@ServletComponentScan
注解解析:
- Servlet可以直接通过
@WebServlet
注解自动注册 - Filter可以直接通过
@WebFilter
注解自动注册 - Listener可以直接通过
@WebListener
注解自动注册
LoginCheckFilter
/**
* 检查用户是否已经完成登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
// 路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER=new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request= (HttpServletRequest) servletRequest;
HttpServletResponse response= (HttpServletResponse) servletResponse;
log.info("拦截到请求:{}",request.getRequestURI());
// 1、获取本次请求的uri
String requestURI = request.getRequestURI();
// 不需要处理的请求
String[] urls=new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
// 2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
// 3、如果不需要处理,则直接放行
if (check){
log.info("本次请求不需要处理");
filterChain.doFilter(request,response);
return;
}
// 4、判断登陆状态,如果已登陆,则直接放行
if (request.getSession().getAttribute("employee")!=null){
log.info("用户已登录");
filterChain.doFilter(request,response);
return;
}
// 5、如果未登录则返回未登录结果
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI){
for (String url:urls){
boolean match = PATH_MATCHER.match(url, requestURI);
if (match){
return true;
}
}
return false;
}
}
2、员工管理
2.1 新增员工
需求分析
后台系统中可以管理员工信息,通过新增员工信息来添加系统用户。点击【添加员工】按钮跳转到新增页面。
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将新增员工页面输入的数据以json的形式提交到服务端;
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存;
3、Service调用Mapper操作数据库,保存数据。
1.查看新增页面请求url
2.编写Controller
@RequestMapping(value = "",method = RequestMethod.POST)
public R<String> add(@RequestBody Employee employee, HttpServletRequest request){
log.info("employee=>{}",employee);
return null;
}
在 log.info("employee=>{}",employee);
打上断点,debug运行程序,查看页面提交到后端的数据;
3.新增页面输入数据
4.查看页面提交的数据
5.完善Controller代码
/**
* 新增员工
* @param employee
* @param request
* @return
*/
@RequestMapping(value = "",method = RequestMethod.POST)
public R<String> add(@RequestBody Employee employee, HttpServletRequest request){
log.info("employee=>{}",employee);
/*
由于页面提交的属性有限,其他属性还需自己手动添加
只有name username phone sex idNumber
*/
// 1.设置初识密码123456(需要md5加密)
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
// 2.获取当前用户id
Long employeeId = (Long) request.getSession().getAttribute("employee");
// 3.设置创建时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
// 4.设置更新时间
// 5.设置当前用户id
employee.setCreateUser(employeeId);
// 6.设置修改用户id
employee.setUpdateUser(employeeId);
// 添加用户 Duplicate entry 'zhangsan' for key 'idx_username' username重复会报错
employeeService.save(employee);
return R.success("添加成功");
}
6.测试
由于表中账号字段设置唯一,test表中已经存在,所以报错 500:Duplicate entry 'test' for key 'idx_username'
,只需换一个测试数据,重新输入,后面会对报错进行统一处理。
再次输入数据提交,测试代码
7.数据库查看是否新增成功
全局异常处理
/**
* 全局异常处理
*/
@Slf4j
@ResponseBody
@ControllerAdvice(annotations = {
RestController.class, Controller.class}) // 捕捉异常的范围
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @param exception : 违反数据库的唯一约束条件
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException exception){
log.error(exception.getMessage());
if (exception.getMessage().contains("Duplicate entry")){
// Duplicate entry 'test' for key 'idx_username'
String [] error=exception.getMessage().split(" ");
// 'test'
return R.error(error[2]+"重复了");
}
return R.error("失败了");
}
}
功能测试
登陆后,添加一个一个已经存在账号名,看前端页面提示信息,以及看后台是否输出了报错日志;
2.2 分页查询员工信息
需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将分页查询参数page、pageSize、name
提交到服务端;
2、服务端Controller接受页面提交的数据并调用Service查询数据;
3、Service调用Mappers操作数据库,查询分页数据;
4、Controller将查询到的分页数据响应给页面;
5、页面接收到分页数据并通过ElemenUI的Table组件展示到页面上。
1.查看页面请求
2.编写后端接口
@RequestMapping(value = "/page",method = RequestMethod.GET)
public R<Page> page(int page,int pageSize,String name){
log.info("page=>{},pageSize=>{},name=>{}",page,pageSize,name);
return null;
}
测试前端数据是否可以接收到
首次进入index.html
页面
利用name进行过滤
3.完善Controller代码
/**
* 分页查询员工信息
* @param page
* @param pageSize
* @param name
* @return
*/
@RequestMapping(value = "/page",method = RequestMethod.GET)
public R<Page<Employee>> page(int page,int pageSize,String name){
log.info("page=>{},pageSize=>{},name=>{}",page,pageSize,name);
// 构造分页构造器
Page<Employee> pageInfo = new Page<>(page, pageSize);
// 构造条件查询器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
// 添加过滤条件
queryWrapper.like(!Strings.isEmpty(name),Employee::getName,name);
// 添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
// 执行查询
pageInfo=employeeService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
再次测试
首次查询
过滤查询
注意:无论怎么查询,始终共有0条数据
问题分析
- 没有创建MybatisPlusInterceptor(MyBatisPlus分页拦截器)实例,导致total值一直为0。
解决方法
- 创建MybatisPlusInterceptor实例。
/**
* 配置mybatis-plus提供的分页插件拦截器
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
2.3 启用/禁用员工账号
需求分析
在员工管理列表页面中,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录管理系统,启用后可以正常登录;
需要注意的是:只有管理员(admin)
才可以对其他普通用户进行启用/禁用操作,所以普通用户登录系统后启用/禁用不显示
。
并且如果某个员工账号状态为正常,则按钮显示为禁用
,如果员工账号状态为已禁用,则按钮显示为启用
。
流程分析
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将参数id、status
提交到服务端;
2、服务端Controller
接收页面提交的数据并调用Service
更新数据;
3、Service
调用Mapper
操作数据库。
代码开发
页面上的展示,前端代码已经处理好了,我们只要处理后端即可。
1.查看前端代码的接口
页面携带了两个参数:
- 当前用户id
- 当前用户的状态
注意:启用/禁用员工账号,本质就是一个更新操作,修改员工状态的方法
2.编写Controller
@RequestMapping(value = "",method = RequestMethod.PUT)
public R<String> status(@RequestBody Employee employee){
log.info("员工状态信息=>{}",employee);
return null;
}
我们发现,当我们进行debug查询时,发现前端传过来的id和我们数据库中的id不一样
。
原因是:mybatis-plus 对id 使用了雪花算法
,所以存入数据库中的id是19
长度,但是前端的js只能保证数据的前16
位数据的精度,对我们id后面的3位数据进行四舍五入,所以就出现了精度丢失;
就会出现前端传过来的id和数据库中的id不一致,就无法修改到数据库中的信息。
解决方法
自定义消息转换器
由于js对long类型的数据精度会丢失,那么我们就把数据进行转型,我们可以在服务端给页面响应的json格式数据进行处理,将long类型数据统一转换为string字符串;
代码实现
1、提供对象转换器