一、瑞吉外卖项目介绍
1、项目背景介绍
本项目(瑞吉外卖
)是专门为餐饮企业(餐厅、饭店
)定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护
。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等
。
本项目供分为3期进行开发
第一期
实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问;
第二期
针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便;
第三期
针对系统进行优化升级,提高系统的访问性能;
2、产品原型介绍
产品原型
一款产品成型之前的一个简单框架,就是将页面的排版布局展现出现,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观的了解项目的需求和提供的功能。
技术选型
3、功能架构
4、角色
后台系统普通员工
C端用户
二、开发环境搭建
项目架构
1、数据库
1.1 创建数据库reggie
数据表
表名 | 信息 |
---|---|
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
注解解析:
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;
}
}
@L_502_24@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("失败了");
}
}
功能测试
登陆后,添加一个一个已经存在账号名,看前端页面提示信息,以及看后台是否输出了报错日志;
@H_342_3502@
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实例。
/**
* 配置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.查看前端代码的接口
页面携带了两个参数:
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、提供对象转换器JacksonObjectMapper
,基于Jackson进行java对象到json数据的转换
2、在WebMvcConfig
配置类中扩展Spring MVC
的消息转换器,在此消息转换器中提供的对象转换器进行java对象到json数据的转换
。
消息转换器
/**
* 对象映射器:
* 基于jackson将java对象转为json,或者将json转换为java对象
* 将java对象生成json的过程称为:【序列化java对象到json】
* 将json解析为java对象的过程称为:【从json反序列话java对象】
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT="yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT="yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper(){
super();
// 收到位置属性时不报异常
this.configure(DeserializationFeature.FAIL_ON_UNKNowN_PROPERTIES,false);
// 反序列化时,属性不存在兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNowN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
配置到spring中
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器");
// 创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter=new MappingJackson2HttpMessageConverter();
// 设置对象转换器,底层使用jackson将java对象转为json
messageConverter.setobjectMapper(new JacksonObjectMapper());
// 将上面的消息转换器对象追加到mvc框架的消息转换器中
// 转换器是有优先顺序的,这里我们把自定义的消息转换器设置为第一优先级,优先转换
converters.add(0,messageConverter);
}
可以在log.info("扩展消息转换器");
打上断点,当我debug启动程序时,观察我们的消息转换器是否被加入
页面传入的数据从long类型转换为了字符串类型,后端也可以正常接收到id
完善Controller代码
/**
* 根据id修改状态(启用/禁用)
* @param employee
* @return
*/
@RequestMapping(value = "",method = RequestMethod.PUT)
public R<String> status(@RequestBody Employee employee,HttpServletRequest request){
log.info("员工状态信息=>{}",employee);
Long employeeId = (Long) request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.Now());
employee.setUpdateUser(employeeId);
employeeService.updateById(employee);
return R.success("修改状态成功!");
}
测试
2.4 编辑员工信息
需求分析
在员工管理列表页面点击编辑按钮,跳转到编辑页面,在标记页面辉县员工信息并进行修改,最后点击保存按钮完成编辑操作
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
1、点击编辑按钮时,页面跳转到
add.html
,并在url中携带参数员工id
;2、在
add.html
页面获取url
中的参数员工id
;3、发送
ajax
请求,请求服务端,同时提交员工id
参数;4、服务端接收请求,根据
员工id
查询员工信息,将员工信息以json形式
响应给页面;5、页面接收服务端响应的
json数据
,通过vue的数据绑定
进行员工信息回显;6、点击
保存
按钮,发送ajax
请求,将页面中的员工信息以json方式
提交给服务端;7、服务端接收员工信息,并进行处理,完成后给页面响应;
8、页面接收到服务端响应信息进行相应处理;
代码开发
当我们点击编辑按钮时,页面会发送一个请求来获取员工信息,在add.html
进行数据回显
编写Controller代码
/**
* 根据id查询员工信息
* @param id
* @return
*/
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public R<Employee> getById(@PathVariable("id") Long id){
log.info("id=>{}",id);
Employee employee = employeeService.getById(id);
return R.success(employee);
}
修改回显数据后,点击保存,会发送一个
update
的请求给后端,前面我们已经写了这个update的controller,所以只需要在前端跳转发请求就行;这样就实现了方法的复用,减少了代码量。测试
3、菜品分类管理
3.1 公共字段填充
前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段
,在编辑员工时需要设置修改时间和修改人
等字段,这些字段属于公共字段
,也就是很多表中都有这些字段,如下:
代码实现
Mybatis Plus 公共字段字段填充,也就是在插入或者更新时为指定字段赋予指定值,使用它的好处就是可以统一对这些字段进行处理,避免重复代码。
实现步骤
1、在实体类的属性上加入@TableField
注解,指定自动填充的策略
2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler
接口
/**
* 自定义元数据对象管理器
*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入时填充数据
* @param MetaObject
*/
@Override
public void insertFill(MetaObject MetaObject) {
log.info("公共字段填充【insert】...");
log.info(MetaObject.toString());
MetaObject.setValue("createTime", LocalDateTime.Now());
MetaObject.setValue("updateTime",LocalDateTime.Now());
MetaObject.setValue("createuser", 1L);
MetaObject.setValue("updateUser",1L);
}
/**
* 更新时填充数据
* @param MetaObject
*/
@Override
public void updateFill(MetaObject MetaObject) {
log.info("公共字段填充【update】...");
log.info(MetaObject.toString());
MetaObject.setValue("updateTime",LocalDateTime.Now());
MetaObject.setValue("updateUser",1L);
}
}
修改员工信息
功能完善
实现步骤
1、编写
BaseContext
工具类,基于ThreadLocal
封装的工具类
/**
* 基于ThreadLocal封装工具类,保存和获取当前用户ID
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal=new ThreadLocal<>();
// 保存用户id
public static void setCurrentId(long id){
threadLocal.set(id);
}
// 获取用户id
public static Long getCurrentId(){
return threadLocal.get();
}
}
2、在LoginCheckFilter
的doFilter
方法中调用BaseContext
来设置当前登录用户的id
3、在MyMetaObjectHandler
的方法中调用BaseContext
获取登录用户id
3.2 新增分类
需求分析
数据模型
新增分类,其实就是将我们新增窗口录入的分类数据插入到category表,表结构如下:
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类
Category
/**
* 分类
*/
@Data
public class Category implements Serializable {
// 序列化id
private static final long serialVersionUID =1L;
// 主键
private Long id;
// 类型。 1:菜品分类 2:套餐分类
private Integer type;
// 分类名称
private String name;
// 顺序
private Integer sort;
// 创建时间
@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;
}
Mapper
接口CategoryMapper
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}
业务层接口CategoryService
public interface CategoryService extends IService<Category> {
}
业务层实现类CategoryeServiceImpl
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
}
控制层CategoryController
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
}
查看前端请求接口
新增分类和新增套餐共用一个接口,唯一的区别就是type
不一样,1:分类,2:套餐
编写Controller
/**
* 新增分类/套餐
* @param category
* @return
*/
@RequestMapping(value = "",method = RequestMethod.POST)
public R<String> save(@RequestBody Category category){
log.info("category=>{}", category);
categoryService.save(category);
return R.success("新增分类成功");
}
测试
注意:
idx_category_namenameUnique
所以添加重复的name
会报错
3.3 分类信息分页查询
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送
ajax
请求,将分页查询参数page、pageSize
提交到服务器2、服务器
Controller
接收页面提交的数据并调用Service
查询数据3、
Service
调用Mapper
操作数据库,查询分页数据4、
Controller
将查询到的分页数据响应给页面5、页面接收分页数据并通过
ElementUI
的Table组件
展示到页面上
页面请求信息
编写Controller
/**
* 分页查询分类信息
* @param page 第几页
* @param pageSize 页面大小
* @return
*/
@RequestMapping(value = "/page",method = RequestMethod.GET)
public R<Page<Category>> page(int page, int pageSize){
log.info("page:{},pageSize:{}",page,pageSize);
// 构造分页构造器
Page<Category> pageInfo = new Page<>();
// 构造条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
// 添加排序条件 (根据sort进行正序排列)
queryWrapper.orderByAsc(Category::getSort);
// 分页查询
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
测试
3.4 删除分类
需求分析
代码开发
/**
* 删除分类
* @param id
* @return
*/
@RequestMapping(value = "",method = RequestMethod.DELETE)
public R<String> delete(@RequestParam("ids") long id){
log.info("删除分类:id为{}",id);
categoryService.removeById(id);
return R.success("删除分类");
}
功能完善
dish
/**
* 菜品类
*/
@Data
public class dish implements Serializable {
private static final long serialVersionUID=1L;
// 主键
private Long id;
// 菜品名称
private String name;
// 菜品分类id
private Long categoryId;
// 菜品价格
private BigDecimal price;
// 商品码
private String code;
// 图片
private String image;
// 描述信息
private String description;
// 状态 0:停售 1:起售
private Integer status;
// 顺序
private Integer sort;
// 创建时间
@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;
// 是否删除
private Integer isDeleted;
}
Setmeal
/**
* 套餐类
*/
@Data
public class Setmeal implements Serializable {
private static final long serialVersionUID=1L;
// 主键
private Long id;
// 菜品分类id
private Long categoryId;
// 套餐名称
private String name;
// 套餐价格
private BigDecimal price;
// 状态 0:停用 1:启用
private Integer status;
// 编码
private String code;
// 描述信息
private String description;
// 图片
private String image;
@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;
// 是否删除
private Integer isDeleted;
}
dishMapper
@Mapper
public interface dishMapper extends BaseMapper<dish> {
}
SetmealMapper
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}
dishMapperService
public interface dishService extends IService<dish> {
}
SetmealService
public interface SetmealService extends IService<Setmeal> {
}
dishMapperServiceImpl
@Service
public class dishServiceImpl extends ServiceImpl<dishMapper, dish> implements dishService {
}
SetmealServiceImpl
@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
}
在CategoryService
中添加自定义remove()
方法
// 根据id删除分类
void remove(Long id);
CategoryServiceImpl
实现remove()
方法,并进行判断
/**
* 根据id删除分类,删除之前要进行判断
* @param id
*/
@Override
public void remove(Long id) {
LambdaQueryWrapper<dish> dishQueryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件,根据分类id进行查询
dishQueryWrapper.eq(dish::getCategoryId,id);
long count1 = dishService.count(dishQueryWrapper);
// 查询当前分类是否关联了菜品,如果已关联,抛出一个业务异常
if (count1>0){
// 已经关联了菜品,抛出一个业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}
// 查询当前分类是否关联了套餐,如果已关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealQueryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件,根据分类id进行查询
setmealQueryWrapper.eq(Setmeal::getCategoryId,id);
long count2 = setmealService.count(setmealQueryWrapper);
if (count1>0){
// 已经关联了套餐,抛出一个业务异常
throw new CustomException("当前分类下关联了套餐,不能删除");
}
// 正常删除
super.removeById(id);
}
自定义异常类CustomException
/**
* 自定义异常类
*/
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}
全局异常捕获GlobalExceptionHandler
,将异常信息在页面展现出来
/**
* 异常处理方法
* @param exception
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException exception){
log.error(exception.getMessage());
return R.error(exception.getMessage());
}
CategoryController
中调用我们自己写的remove()
方法
测试
删除湘菜时,由于关联了数据,所以不能删除并给出错误信息
3.5 修改分类
需求分析
页面请求
/**
* 根据id修改分类信息
* @param category
* @return
*/
@RequestMapping(value = "",method = RequestMethod.PUT)
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
4、菜品管理
4.1 文件上传下载
文件下载介绍
文件上传代码实现
启动项目,访问
http://localhost:8080/backend/page/demo/upload.html
(需要先登录)
页面请求
编写
CommonController
@RestController
@Slf4j
@RequestMapping("/common")
public class CommonController {
/**
* 文件上传
* @param file
* @return
*/
@RequestMapping(value = "/upload",method = RequestMethod.POST)
public R<String> upload(multipartfile file){
// file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
return null;
}
}
注意: file的名字不可更改,否者会为null
在log.info(file.toString());
打上断点,并运行查看,
由于
file
是一个临时文件,执行结束后,就会自动删除,所以我们需要将文件保存到指定位置。
# 上传图片存放的位置
reggie:
path: D:\WorkSpaces\IdeaProject\reggie_take_out\work\
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传
* @param file
* @return
*/
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public R<String> upload(multipartfile file) {
// file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
// 获取上传文件名称
String originalFilename = file.getoriginalFilename();
// 获取后缀
val suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
// uuid生成新的文件名称
String fileName = UUID.randomUUID().toString() + suffix;
try {
// 判断文件夹是否存在
File dir = new File(basePath);
if (!dir.exists()) {
// 自动生成文件夹
dir.mkdirs();
}
// 将临时文件转存到指定位置
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printstacktrace();
}
return R.success(fileName);
}
name
为刚才上传的图片名称
/**
* 文件下载
* @param name
* @param response
* @return
*/
@RequestMapping(value = "/download",method = RequestMethod.GET)
public void download(String name, HttpServletResponse response){
log.info("name:{}",name);
try {
// 读取文件
FileInputStream fis=new FileInputStream(new File(basePath+name));
// 写文件
ServletoutputStream os = response.getoutputStream();
response.setContentType("image/jpeg");
int len;
byte[] bytes=new byte[1024];
while ((len=fis.read(bytes))!=-1){
os.write(bytes,0,len);
os.flush();
}
// 关闭资源
os.close();
fis.close();
} catch (FileNotFoundException e) {
e.printstacktrace();
} catch (IOException e) {
e.printstacktrace();
}
}
测试
4.2 新增菜品
需求分析
数据模型
代码开发-准备工作
dishFlavor
/**
* 菜品口味
*/
@Data
public class dishFlavor implements Serializable {
// 序列化id
private static final long serialVersionUID=1L;
// 主键
private Long id;
// 菜品id
private Long dishId;
// 口味名字
private String name;
// 口味数据
private String value;
@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;
// 是否删除
private Integer isDeleted;
}
dishFlavorMapper
@Mapper
public interface dishFlavorMapper extends BaseMapper<dishFlavor> {
}
dishFlavorSerivce
public interface dishFlavorService extends IService<dishFlavor> {
}
dishFlavorServiceImpl
@Service
public class dishFlavorServiceImpl extends ServiceImpl<dishFlavorMapper, dishFlavor> implements dishFlavorService {
}
dishController
@RequestMapping("/dish")
@RestController
@Slf4j
public class dishController {
@Autowired
private dishService dishService;
@Autowired
private dishFlavorService dishFlavorService;
}
梳理交互过程
在开发代码之前,需要梳理一下新增菜品对前端页面和服务端的交互过程:
1、页面backend/page/food/add.html
发送ajax
请求,请求服务器获取菜品分类数据并展示到下拉框中
2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
3、页面发送请求进行图片下载,将上传的图片进行回显
4、点击保存按钮,发送ajax请求,将菜品相关数据以json
形式提交到服务端
CategoryController
/**
* 获取菜品分类列表
* @param category
* @return
*/
@RequestMapping(value = "/list",method = RequestMethod.GET)
public R<List<Category>> list(Category category){
log.info("获取菜品分类");
// 构造条件查询器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
// 添加条件
queryWrapper.eq(category.getType()!=null,Category::getType,category.getType());
// 添加排序条件
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
测试
结果:菜品分类已经可以正常显示出来了
新增菜品
页面请求
页面提交的数据设计
dish
和dishFlavor
两张表,所以我们需要封装另外一个类dishDto
来接收数据
DTO:Data Transfer Object 即数据传输对象,一般用于展示层和服务层之间的数据传输。
@Data
public class dishDto extends dish {
// 封装页面口味等。
private List<dishFlavor> flavors=new ArrayList<>();
private String categoryName;
private Integer copies;
}
dishService
// 新增菜品,同时插入菜品对应的口味数据
void saveWithFlavor(dishDto dishDto);
dishServiceImpl
/**
* 新增菜品,同时插入菜品对应的口味数据
* @param dishDto
*/
@Override
@Transactional
public void saveWithFlavor(dishDto dishDto) {
// 保存菜品的基本信息
this.save(dishDto);
// 保存菜品口味数据到菜品口味表dish_flavor
Long dishId = dishDto.getId(); // 菜品id
// 菜品口味
List<dishFlavor> flavors = dishDto.getFlavors();
flavors=flavors.stream().map((item)->{
item.setdishId(dishId);
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
dishController
/**
* 新增菜品
* @param dishDto
* @return
*/
@RequestMapping(value = "", method = RequestMethod.POST)
public R<String> save(@RequestBody dishDto dishDto) {
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
测试
数据添加到
dish
表中数据添加到
dish_flavor
表中4.3 菜品类的分页
需求分析
梳理交互过程
在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务器的交互过程:
1、页面backend/page/food/list.html
发送ajax
请求,将分页查询参数page,pageSize,name
提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
页面请求
/**
* 分页查询菜品信息
* @param page
* @param pageSize
* @param name
* @return
*/
@RequestMapping(value = "/page",method = RequestMethod.GET)
public R<Page<dish>> page(int page,int pageSize,String name){
log.info("page=>{},pageSize=>{},name=>{}",page,pageSize,name);
// 构造分页构造器
Page<dish> pageInfo = new Page<>(page,pageSize);
// 构造条件构造器
LambdaQueryWrapper<dish> queryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件
queryWrapper.like(name!=null,dish::getName,name);
// 添加排序条件
queryWrapper.orderByDesc(dish::getUpdateTime);
dishService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
测试
我们发现,菜品分类信息没有展现出现,因为
dish
表中只有菜品分类id
完善代码
/**
* 分页查询菜品信息
*
* @param page
* @param pageSize
* @param name
* @return
*/
@RequestMapping(value = "/page", method = RequestMethod.GET)
public R<Page<dishDto>> page(int page, int pageSize, String name) {
log.info("page=>{},pageSize=>{},name=>{}", page, pageSize, name);
// 构造分页构造器
Page<dish> pageInfo = new Page<>(page, pageSize);
Page<dishDto> dishDtoPage = new Page<>();
// 构造条件构造器
LambdaQueryWrapper<dish> queryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件
queryWrapper.like(name != null, dish::getName, name);
// 添加排序条件
queryWrapper.orderByDesc(dish::getUpdateTime);
dishService.page(pageInfo, queryWrapper);
// 对象拷贝
BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");
List<dish> records = pageInfo.getRecords();
List<dishDto> list = records.stream().map((item) -> {
dishDto dishDto = new dishDto();
BeanUtils.copyProperties(item, dishDto);
Long categoryId = item.getCategoryId();// 分类id
Category category = categoryService.getById(categoryId);
// 获取分类名称
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
return dishDto;
}).collect(Collectors.toList());
// 设置records
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
测试
4.4 修改菜品
需求分析
梳理交互过程
在开发代码之前,需要梳理一下修改菜品时前端页面add.html
和服务端交互过程:
1、页面发送ajax
请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送ajax
请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax
请求,将修改后的菜品相关数据以json
形式提交到服务端
点击修改按钮,根据id获取菜品信息,进行菜品信息回显
页面请求
由于设计
dish
和 dish_flavor
两张表,所以返回值应该为dishDto
dishService
// 根据id查询菜品信息和对应的口味信息
dishDto getByIdWithFlavor(Long id);
dishServiceImpl
/**
* 根据id查询菜品信息和对应的口味信息
* @param id
* @return
*/
@Override
public dishDto getByIdWithFlavor(Long id) {
// 查询菜品基本信息,从dish表中查询
dish dish = this.getById(id);
dishDto dishDto = new dishDto();
BeanUtils.copyProperties(dish,dishDto);
// 查询当前菜品信息对应的口味信息,从dish_flavor表查询
LambdaQueryWrapper<dishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dishFlavor::getdishId,dish.getId());
List<dishFlavor> flavors = dishFlavorService.list(queryWrapper);
// 设置口味
dishDto.setFlavors(flavors);
return dishDto;
}
dishController
/**
* 根据id查询菜品信息
* @param id
* @return
*/
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public R<dishDto> getById(@PathVariable("id") Long id){
log.info("根据id获取菜品信息:{}",id);
dishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
测试
修改完菜品信息后,点击保存按钮,发送请求
页面请求
携带的参数信息
dishService
// 更新菜品信息和对应口味信息
void updateWithFlavor(dishDto dishDto);
dishServiceImpl
/**
* 更新菜品信息和对应口味信息
* @param dishDto
*/
@Override
@Transactional
public void updateWithFlavor(dishDto dishDto) {
// 更新dish表基本信息
this.updateById(dishDto);
// 清理当前dish_flavor表口味信息 - dish_flavor delete操作
// delete from dish_flavor where dish_id=?
LambdaQueryWrapper<dishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dishFlavor::getdishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
// 添加当前提交过来的口味数据 - dish_flavor insert操作
List<dishFlavor> flavors = dishDto.getFlavors();
// dish_flavor表中 缺少dish_id
flavors=flavors.stream().map((item)->{
item.setdishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
dishController
/**
* 修改菜品
* @param dishDto
* @return
*/
@RequestMapping(value = "", method = RequestMethod.PUT)
public R<String> update(@RequestBody dishDto dishDto) {
log.info(dishDto.toString());
dishService.updateWithFlavor(dishDto);
return R.success("修改菜品成功");
}
测试
口味表