从开发人员角度来说,接触最多的就是开发环境。
本项目(瑞吉外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。也就是说,整个项目分为管理员端和客户端两个部分。
本项目共分为3期进行开发:
第一期主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问。
第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便。
第三期主要针对系统进行优化升级,提高系统的访问性能。
产品原型就是一款产品成型之前的一个简单的框架,就是将页面的排版布局展现出来,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观地了解项目的需求和提供的功能。产品原型是产品经理在需求分析阶段来做的,一般的产品原型都是一些网页(HTML页面)。
注:产品原型主要用于展示项目的功能,并不是最终的页面效果。
项目需要用到的技术栈:
展示项目需要具有哪些功能
数据库环境搭建:
创建项目对应的数据库(图形界面或者命令行都可以)
1、首先新建数据库,名称为reggie,字符集是utf-8mb4
2、新建数据库后运行相关SQL文件,导入相关的表结构
项目涉及到了11张表
导入后的表:
3、数据表对照定义(表结构):
1、创建Maven项目
项目名:reggie_take_out
Maven项目就已经创建成功了。
注:创建完项目后,注意检查项目的编码、maven仓库配置、jdk配置等。
2、导入相关的pom文件,导入所需要的依赖
本地资料有pom文件,直接复制即可。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.5version>
<relativePath/>
parent>
<groupId>com.itheimagroupId>
<artifactId>reggie_take_outartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<scope>compilescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.2version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.20version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.76version>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.23version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.4.5version>
plugin>
plugins>
build>
project>
3、导入相关的application.yml文件,对SpringBoot进行配置
本地资料有application.yml文件,这个配置文件可以对SpringBoot进行配置。
server:
port: 8099
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&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
4、编写启动类(SpringBoot项目入口)
//@Slf4j输出日志方便调试,是lombok提供的注解
@Slf4j
@SpringBootApplication
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功!");
}
}
由于SpringBoot默认静态前端页面是放在static和template目录下的,所以直接将前端资源放在resources目录下去访问是不可行的,此时我们就需要进行配置,使得MVC框架识别到特定文件夹下的资源是我们需要访问的前端资源,此时我们需要写一个配置类。
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射");
//将url需要访问的地方映射到相应的文件夹下,这样就不会拒绝访问了
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
注:记得在启动程序加上@ServletComponentScan这个注解,否则这个配置类不会生效;
需求分析一般都是从页面原型说起、
页面原型:
登陆页面展示:
当我们输入用户名密码后,点击登录按钮
此时我们可以发现,请求到了employee/login路径下面,并且将用户名和密码,以json格式提交到了服务端,报404是因为后端的接口尚未开发。
需求流程:
数据模型参照employee表
前端代码:
这里可以看到,前端所需要的参数分别有code、data以及msg。它从这个返回结果里获得这些值,就要求我们服务端处理完之后,响应数据里应该含有这三个值,这样前端页面才能够取到,也就是在明确服务端在处理完之后,应该给服务页面响应什么样的结果数据。(数据应该是一个json格式的数据,到时候在写后端代码的时候还会再强调)
1、创建实体类Employee,和employee表进行映射,可以直接导入资料中提供的实体类。
package com.itheima.reggie.Entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 员工实体类
*/
@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;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
新建controller,entity,service,mapper包全部创建出来,将employee实体类创建出来
然后将后台登陆功能的controller,service,serviceimpl,mapper类全部创建
mapper类:
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Employee;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
虽然这部分代码量不大,但由于继承了BaseMapper,所以基本的CRUD功能都在,功能还是十分强大的。
service类:
package com.itheima.reggie.service;
/*
Employee的service接口
*/
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Employee;
public interface EmployeeService extends IService<Employee> {
}
这个接口也需要继承MybatisPlus提供的IService接口,也需要指定泛型为Employee实体
service实现类:
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Employee;
import com.itheima.reggie.mapper.EmployeeMapper;
import com.itheima.reggie.service.EmployeeService;
import org.springframework.stereotype.Service;
//加@Service注解,由Spring管理它
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
service实现类需要继承ServiceImpl类,其中有两个泛型,分别是EmployeeMapper和Employee实体类,然后实现刚刚定义好的EmployeeService接口。
controller类:
package com.itheima.reggie.controller;
import com.itheima.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
}
在RequestMapping这里,为什么是"/employee"呢?因为登录的时候请求路径里面含有employee,控制层需要和URI对应,然后里面通过@Autowired注解,把咱们创建的service接口注入进来就可以了。
导入通用返回结果类R,这个类R就是一个通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面。
创建common包,然后将R类复制进去
package com.itheima.reggie.common;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class R<T> {
private Integer code; //编码:1成功,0和其它数字为失败
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;
}
}
code:代表的是返回值,1代表成功,其它数字代表失败。
msg:代表错误信息,比如登陆失败的时候,msg里面会有显示登陆失败的信息
data:具体的一些数据,比如登录的时候,这个T就可能是employee员工对象,后面在开发的时候还有其他对象,增强通用性
map:封装的一些动态数据,特定场合会使用
在控制层中写登录接口的控制层代码:
/**
* 员工登录
* @param request
* @param employee
* @return
*/
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
return null;
}
首先定义PostMapping,确定好URI地址,然后返回类是R,泛型是Employee,需要的参数是employee员工类,注意:前端所需的参数是json格式的username和password,所以需要在employee参数前加一个@RequestBody注解。除了员工类,还需要一个参数HttpServletRequest request,要这个参数的原因是登录成功后,需要将employee对象的id存到session一份,表示登陆成功,这样如果想获取当前登录用户的话随时就可以获取出来,就可以通过request对象来get一个session。
注:run和debug的区别。run会直接把一次性的程序全部跑完,debug可以加入断点,来分析程序运行的过程。
现在需要做的就是将处理逻辑真正通过代码的方式来实现出来:
1、分析真正处理的逻辑
2、将具体处理的步骤梳理好
3、将梳理好的步骤通过注解的方式写进去,然后逐步实现
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
// 1、将页面提交的密码password进行md5加密处理
String password = employee.getPassword();
//调用DigestUtils工具类,里面有md5加密的工具方法
password = DigestUtils.md5DigestAsHex(password.getBytes());
// 2、根据页面提交的用户名username查询数据库
//首先包装一个查询对象
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,这里是一个等值查询,根据用户名和传过来的用户名,条件就封装好了
queryWrapper.eq(Employee::getUsername,employee.getUsername());
//调用service的getOne方法
Employee emp = employeeService.getOne(queryWrapper);
// 3、如果没有查询到则返回登录失败结果
if (emp == null){
return R.error("登陆失败!");
}
// 4、密码比对,如果不一致则返回登录失败结果
if (!emp.getPassword().equals(password)){
return R.error("登陆失败,用户名或密码错误!");
}
// 5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
if (emp.getStatus() == 0) {
return R.error("禁止登陆,账号已禁用!");
}
// 6、登录成功,将员工id存入Session并返回登录成功结果
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);
}
调用getOne方法的原因:因为数据库中对username字段做了一个唯一性约束,既然是唯一性约束,就可以调用getOne方法来查出唯一的数据,查出后封装成employee对象
在前台页面,进入调试模式,在密码处打了一个断点,然后我们可以查看拿到的数据:可以看到用户名是admin,密码是333333
往下走,可以看到根据用户名查询其实是查到了的
然后发现密码比对处的逻辑判断,密码是错误的了,所以就返回了R.error登陆失败。
输入正确的密码登录进去以后,这里就转成JSON格式的数据展现在浏览器里面了
浏览器里输入F12,查看Application里面的Storage的Local Storage,可以看到数据(LocalStorage其实就是浏览器存储的一种方式)
json数据是由SpringMVC将R对象转换成json数据的
员工登录成功后,页面跳转到后台系统首页面(backend/index.html),此时会显示当前登录用户的姓名:
如果员工需要退出系统,直接点击右侧的退出按钮即可退出系统,退出系统后页面应跳转回登录页面
点击退出按钮,出现404报错,但其实他是发了一个请求出去的
用户点击页面中退出按钮,发送请求,请求地址为/employee/logout,请求方式为POST。
我们只需要在Controller中创建对应的处理方法即可,具体的处理逻辑:
1、清理Session中的用户id
2、返回结果
相应代码如下:
/**
* 员工退出方法
* @param request 由于等会要用到session,所以需要request参数
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清理Session中保存的当前的员工id
request.getSession().removeAttribute("employee");
return R.success("退出成功!");
}
7、分析后台系统首页构成和效果展示方式
下列是index.html内的vue代码,左侧的菜单栏就是由这里定义的,menuList列表里面放着一个个的json,每个json里面都有相应的数据
menuList: [
// {
// id: '1',
// name: '门店管理',
// children: [
{
id: '2',
name: '员工管理',
url: 'page/member/list.html',
icon: 'icon-member'
},
{
id: '3',
name: '分类管理',
url: 'page/category/list.html',
icon: 'icon-category'
},
{
id: '4',
name: '菜品管理',
url: 'page/food/list.html',
icon: 'icon-food'
},
{
id: '5',
name: '套餐管理',
url: 'page/combo/list.html',
icon: 'icon-combo'
},
{
id: '6',
name: '订单明细',
url: 'page/order/list.html',
icon: 'icon-order'
}
// ],
// },
],
真正展示菜单的是上面的elementUI上面给的组件
<div v-for="item in menuList" :key="item.id">
<el-submenu :index="item.id" v-if="item.children && item.children.length>0">
<template slot="title">
<i class="iconfont" :class="item.icon">i>
<span>{{item.name}}span>
template>
<el-menu-item
v-for="sub in item.children"
:index="sub.id"
:key="sub.id"
@click="menuHandle(sub,false)"
>
<i :class="iconfont" :class="sub.icon">i>
<span slot="title">{{sub.name}}span>
el-menu-item
>
el-submenu>
<el-menu-item v-else :index="item.id" @click="menuHandle(item,false)">
<i class="iconfont" :class="item.icon">i>
<span slot="title">{{item.name}}span>
el-menu-item>
div>
v-for="sub in item.children"
:index="sub.id"
:key="sub.id"
@click="menuHandle(sub,false)"
>
<i :class="iconfont" :class="sub.icon">i>
<span slot="title">{{sub.name}}span>
el-menu-item
>
el-submenu>
<el-menu-item v-else :index="item.id" @click="menuHandle(item,false)">
<i class="iconfont" :class="item.icon">i>
<span slot="title">{{item.name}}span>
el-menu-item>
div>
点击菜单的时候其实就是在切换URL,做到新的效果。