目录
软件开发整体介绍
软件开发流程
需求分析:产品原型、规格说明书
设计:产品文档、UI界面设计、概要设计、详细设计、数据库设计
编码:项目代码、单元测试
测试:测试用例、测试报告
上线运维:软件环境安装、配置
角色分工
项目经理:对整个项目负责,任务分配、把控进度
产品经理:进行需求调研,输出需求调研文档、产品原型等
UI设计师:根据产品原型输出界面效果图
架构师:项目整体架构设计、技术选型等
开发工程师:代码实现
测试工程师:编写测试用例,输出测试报告
运维工程师:软件环境搭建、项目上线
软件环境
开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
测试环境(testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
生产环境(production):即线上环境,正式提供对外服务的环境
瑞吉外卖项目介绍
项目介绍
产品原型展示
注意事项:产品原型主要用于展示项目的功能,并不是最终的页面效果
技术选型
功能架构
角色
开发环境搭建
数据库环境搭建
maven项目搭建
创建maven项目
注意事项:创建完项目后,注意检查项目的编码、maven仓库、JDK配置等
添加pom文件依赖
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
<groupId>com.green</groupId>
<artifactId>reggie_take_out</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<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>
<scope>compile</scope>
</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>
<version>1.18.20</version>
</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>mysql</groupId>
<artifactId>mysql-connector-java</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>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
</project>
导入SpringBoot配置文件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&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 #主键生成策略
创建Boot程序入口
@Slf4j//日志
@SpringBootApplication
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功...");
}
}
运行Boot程序,看是否成功
导入前端文件
注意前端文件的位置,在Boot项目中,前端默认只能访问 resource目录下的static和template文件夹下的文件;
所以如果要使用这种方式,直接创建一个static目录就行,然后把这些前端资源放在这个static目录下就行;
如果不想把前端文件放在这两个默认的文件夹下,那么就可以自己定义mvc的支持,
这里我们使用的就是这方式;(多学习一种定义的方法,以后自定义映射的时候可以使用)
@Slf4j
@Configuration
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/");
}
}
后台登录功能开发
需求分析
代码开发
导入返回结果类R
此类是一个结果通用类,服务端响应的所有结果都会包装成此种类型返回给前端页面
@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;
}
}
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
/**
* 员工登录
*
* @param request
* @param employee
* @return
*/
@PostMapping
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
// 1、将页面提交的密码password进行md5加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
// 2、根据页面提交的用户名username查询数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername, employee.getUsername());
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);
}
}
后台退出功能开发
需求分析
需求:
员工登录成功后,页面跳转到后台系统首页面(backend/indexhtml),此时会显示当前登录用户的姓名
如果员工需要退出系统,直接点击右侧的退出按钮即可退出系统,退出系统后页面应跳转回登录页面
分析:
用户点击页面中退出按钮,发送请求,请求地址为/emplovee/logout,请求方式为POST.
只需要在Controller中创建对应的处理方法即可,具体的处理逻辑:
1、清理Session中的用户id
2、返回结果
代码开发
/**
* 员工退出登录
* @param request
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清除Session中保存的登录员工id
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
员工管理业务开发
完善登录功能
问题分析
前面已经完成了后台系统的员工登录功能开发,但是还存在一个问题:
用户如果不登录,直接访问系统首页面,照样可以正常访问。
这种设计并不合理,希望看到的效果应该是,只有登录成功后才可以访问系统中的页面,
如果没有登录则跳转到登录页面。
那么,具体应该怎么实现呢?
答案就是使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,
如果没有登录则跳转到登录页面。
实现步骤:
1、创建自定义过滤器LoginCheckFilter
2、在启动类上加入注解@ServletComponentScan
3、完善过滤器的处理逻辑
代码实现
过滤器具体的处理逻辑如下:
1、获取本次请求的URI
2、判断本次请求是否需要处理
3、如果不需要处理,则直接放行
4、判断登录状态,如果已登录,则直接放行
5、如果未登录则返回未登录结果
/**
* 过滤器 :检查用户是否完成登录
*/
@Slf4j
@WebFilter(filterName = "LoginCheckFilter", urlPatterns = "/*")
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;
// 1、获取本次请求的URI
String requestURI = request.getRequestURI();
log.info("本次拦截的路径{}", requestURI);
//定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
// 2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
// 3、如果不需要处理,则直接放行
if (check) {
log.info("本次请求{}不需要处理", requestURI);
filterChain.doFilter(request, response);//放行
return;
}
// 4、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
filterChain.doFilter(request, response);//放行
return;
}
// 5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
// log.info("拦截到请求{}", request.getRequestURL());
}
/**
* 路径匹配,检查本次请求是否放行
* @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;
}
}
新增员工
需求分析
后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击[添加员工]按钮跳转到新增页面,如下:
数据模型
新增员工,其实就是将新增页面录入的员工数据插入到employee表。
需要注意,employee表中对username字段加入了唯一约束,
因为username是员工的登录账号,必须是唯一的
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
/**
* 新增员工
* @param employee
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee) {
log.info("新增员工,员工信息{}", employee.toString());
//设置初始密码:123456,需要进行名MD5加密
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//获取当前登录用户id
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
employeeService.save(employee);
return R.success("新增员工成功");
}
}
/**
* 全局异常处理 通知类
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class}) //根据注解类型进行处理
@ResponseBody//转化JSON格式
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class) //需要处理的异常类型
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
log.error(ex.getMessage());
//contains用于判断一个字符串中是否包含某个子串
if (ex.getMessage().contains("Duplicate entry")) {
String[] split = ex.getMessage().split(" ");//根据空格分割字符串
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
小结
1、根据产品原型明确业务需求
2、重点分析数据的流转过程和数据格式
3、通过debug断点调试跟踪程序执行过程
员工信息分页查询
需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,
不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、pageSizename)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUl的Table组件展示到页面上
启用/禁用员工账户
需求分析
在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。
账号禁用的员工不能登录系统,启用后的员工可以正常登录。
需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁套用操作,
所以普通用户登录系统后启用、禁用按钮不显示。
管理员admin登录系统可以对所有员工账号进行启用、禁用操作。
如果某个员工账号状态为正常,则按钮显示为“禁用”,如果员工账号状态为已禁用,则按钮显示为“启用”
普通员工登录系统后,启用、禁用按钮不显示
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将参数(id、status)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service更新数据
3、Service调用Mapper操作数据库
启用、禁用员工账号,本质上就是一个更新操作,
也就是对status状态字段进行操作在Controller中创建update方法,
此方法是一个通用的修改员工信息的方法
/**
* 根据员工id修改数据
*
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request, @RequestBody Employee employee) {
log.info(employee.toString());
Long empId = (Long) request.getSession().getAttribute("employee");//修改人id
employee.setUpdateTime(LocalDateTime.now());//修改时间
employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
功能测试
代码修复
前面已经发现了问题的原因,即js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致。
如何解决这个问题?
可以在服务端给页面响应ison数据时进行处理,将lona型数据统一转为Strina字符串,效果如下:
具体实现步骤:
1)提供对象转换器jacksonObjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)
2)在WebMvcConfia配置类中扩展Springmvc的消息转换器,在此消息转换器中使用提供的对象转换器进行lava对象到json数据的转换
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
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(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);
}
}
WebMvcConfig配置类中扩展Springmvc的消息转换器
/**
* 扩展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);
}
编辑员工信息
需求分析
在员工管理列表页面点击编辑按钮,跳转到编辑页面,
在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作
代码开发
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id
2、在add.html页面获取url中的参数[员工id
3、发送ajax请求,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以ison形式响应给页面
5、页面接收服务端响应的ison数据,通过VUE的数据绑定进行员工信息回显
6、点击保存按钮,发送aiax请求,将页面中的员工信息以ison方式提交给服务端
7、服务端接收员工信息,并进行处理,完成后给页面响应
8、页面接收到服务端响应信息后进行相应处理
注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作
/**
* 根据id查询员工信息
*
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id) {
log.info("根据id查询员工信息...");
Employee employee = employeeService.getById(id);
if (employee != null) {
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
分类管理业务开发
公共字段自动填充
问题分析
代码实现
Mvbatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,
使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
实现步骤:
1、在实体类的属性上加入@TableField注解,指定自动填充的策略
2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObiectHandler接口
功能测试
功能完善
前面已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,
就是在自动填充Createuser和updateUser时设置的用户id是固定值,
现在需要改造成动态获取当前登录用户的id。有的同学可能想到,
用户登录成功后我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?
注意,我们在MyMeta0bjectHandler类中是不能获得HttpSession对象的,
所以我们需要通过其他方式来获取登录用户id。
可以使用ThreadLoca[来解决此问题,它是JDK中提供的一个类
实现步骤:
1、编写BaseContext工具类,基于ThreadLocal封装的工具类
2、在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id3在MyMetaobjectHandler的方法中调用BaseContext获取登录用户的id
@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", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
/**
* 更新操作,自动填充
*
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
Long id = Thread.currentThread().getId();
log.info("线程id为:{}", id);
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置id值
* @param id
*/
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
/**
* 获取id值
* @return
*/
public static Long getCurrentId() {
return threadLocal.get();
}
}
新增分类
需求分析
后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类。
当我们在后台系统中添加菜品时需要选择一个菜品分类,
当我们在后台系统中添加一个套餐时需要选择一个套餐分类,
在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐
数据模型
代码开发
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类Category(直接从课程资料中导入即可)
Mapper接口CategoryMapper
业务层接口CategoryService
业务层实现类CategoryServicelmpl
控制层CategoryController
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面(backend/page/category/list.html)发送ajax请求,
将新增分类窗口输入的数据以json形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
/**
* 新增分类
*
* @param category
* @return
*/
@PostMapping
public R<String> save(@RequestBody Category category) {
log.info("category:{}", category);
categoryService.save(category);
return R.success("新增分类成功");
}
分类信息分页查询
需求分析
系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,
所以一般的系统中都会以分页的方式来展示列表数据。
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程
1、页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过Elementul的Table组件展示到页面上
/**
* 分页查询
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize) {
log.info("page = {},pageSize = {}", page, pageSize);
//创建分页构造器
Page<Category> pageInfo = new Page<>(page, pageSize);
//创建条件构造器
LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper<>();
//添加排序条件,根据sort排序
lqw.orderByAsc(Category::getSort);
//执行查询
categoryService.page(pageInfo, lqw);
return R.success(pageInfo);
}
删除分类
需求分析
在分类管理列表页面,可以对某个分类进行删除提作。
需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库
/**
* 根据id删除分类
*
* @param ids
* @return
*/
@DeleteMapping
public R<String> deleteById(Long ids) {
log.info("删除分类id:{}", ids);
categoryService.removeById(ids);
return R.success("删除分类成功");
}
功能完善
前面已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,
所以需要进行功能完善。要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish和Setmeal (从课程资料中复制即可
2、Mapper接口DishMapper和SetmealMapper
3、Service接口DishService和SetmealService
4、Service实现类DishServiceImpl和SetmealServiceImpl
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
全局处理通知类添加处理异常业务
/**
* 处理自定义业务异常
* @param ex
* @return
*/
@ExceptionHandler(CustomException.class) //需要处理的异常类型
public R<String> exceptionHandler(CustomException ex) {
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 根据id删除分类,删除之前进行判断是否关联了菜品或套餐
*
* @param id
*/
@Override
public void remove(Long id) {
//创建条件构造器
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
int count1 = dishService.count(dishLambdaQueryWrapper);
//查询当前分类是否关联了菜品,如果已经关联了,抛出一个业务异常
if (count1 > 0) {
//已经关联了菜品,抛出一个业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}
//查询当前分类是否关联了套餐,如果已经关联了,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if (count2 > 0) {
//已经关联了套餐,抛出一个业务异常
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//正常删除分类
super.removeById(id);
}
}
业务层代码
@DeleteMapping
public R<String> deleteById(Long ids) {
log.info("删除分类id:{}", ids);
// categoryService.removeById(ids);
categoryService.remove(ids);
return R.success("删除分类成功");
}
修改分类
需求分析
代码开发
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("当前修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
菜品业务开发
文件上传下载
文件上传介绍
目前一些前端组件库也提供了相应的上传组件,
但是底层原理还是基于form表单的文件上传例如ElementUl中提供的upload上传组件:
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
commons-fileupload
commons-io
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,
我们只需要在Controller的方法中声明个MultipartFile类型的参数即可接收上传的文件,例如:
文件下载介绍
文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
直接在浏览器中打开
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
文件上传代码实现
文件上传,页面端可以使用ElementUl提供的上传组件可以直接使用资料中提供的上传页面,
位置:资料/文件上传下载页面/upload.html
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String basePath;
@PostMapping("/upload")
public R<String> upload(MultipartFile file) {
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件就会删除
log.info(file.toString());
//文件原始名
String originalFilename = file.getOriginalFilename();
//文件后缀名
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID(通用唯一标识符)重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;
//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if (!dir.exists()) {
//目录不存在,需要创建目录
dir.mkdirs();
}
try {
//将临时文件转存到指定位置
// file.transferTo(new File("D://hello.jpg"));
// file.transferTo(new File(basePath + "hello.jpg"));//配置文件动态获取文件路径
// file.transferTo(new File(basePath + originalFilename));
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
}
文件下载代码实现
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
//输出流,通过输出流将文件写会浏览器,在浏览器展示图片
ServletOutputStream servletOutputStream = response.getOutputStream();
//设置响应的文件类型
response.setContentType("image/jpeg");
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1) {
servletOutputStream.write(bytes, 0, len);
servletOutputStream.flush();//刷新流
}
//关闭资源
servletOutputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
新增菜品
需求分析
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,
在添加菜品时需要选择当前菜品所属的菜品分类并且需要上传菜品图片,
在移动端会按照菜品分类来展示对应的菜品信息。
数据模型
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,
如果添加了口味做法,还需要向dish flavor表插入数据。
所以在新增菜品时,涉及到两个表:
dish 菜品表
dish flavor 菜品口味表
代码开发
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类DishFlavor(直接从课程资料中导入即可,Dish实体前面课程中已经导入过了)
Mapper接口 DishFlavorMapper
业务层接口 DishFlavorService
业务层实现类 DishFlavorServiceImpl
控制层 DishController
在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:
1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
3、页面发送请求进行图片下载,将上传的图片进行回显
4、点击保存按钮,发送aiax请求,将菜品相关数据以ison形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
//分类下拉框
/**
* 根据条件查询分类数据
* @param category
* @return
*/
@GetMapping("/list")
public R<List> list(Category category){
//条件构造器
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);
}
注意事项:DTO,全称为Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输。
菜品信息分页查询
需求分析
系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,
不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
代码开发
在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:
1、页面(backend/page/food/list.html)发送ajax请求,
将分页查询参数(page、pageSize、name提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可
/**
* 分页查询
*
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> 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(StringUtils.isNotEmpty(name), Dish::getName, name);
// queryWrapper.like(name != null, Dish::getName, name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行查询
dishService.page(pageInfo, queryWrapper);
//对象拷贝
//将PageInfo对象中的属性复制到dishDtoPage对象中,但是排除了"records"属性
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
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null) {
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
})).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
修改菜品
需求分析
在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,
在修改页面回显菜品相关信息并进行修改,最后点击确定按钮完成修改操作
代码开发
在开发代码之前,需要梳理一下修改菜品时前端页面 (add.html) 和服务端的交互过程:
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以ison形式提交到服务端
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可
//DishServiceImpl
/**
* //更新菜品信息,同时更新对应的口味信息
*
* @param dishDto
*/
@Override
@Transactional//事务控制
public void updateWishFlavor(DishDto dishDto) {
//更新dish表基本信息
this.updateById(dishDto);
//清理当前菜品对应口味数据----dish_flavor表的delete操作
//注意:前端传过来的是dish的id,此处不能用removeById()方法,这个方法的id是口味id不是对应的菜品id
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId, dishDto.getId());
dishFlavorService.remove(queryWrapper);
//添加当前提交过来的口味数据---dish_flavor表的insert操作
List<DishFlavor> dishFlavors = dishDto.getFlavors();
dishFlavors.stream().map((item) -> {
//添加菜品id进菜品口味
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
//保存菜品口味到菜品口味表dish_flavor
dishFlavorService.saveBatch(dishFlavors);
}
/**
* 根据id查询菜品信息和对应的口味信息
* 数据回显
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id) {
DishDto dishDto = dishService.getByIdWishFlavor(id);
return R.success(dishDto);
}
/**
* 修改菜品
*
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto) {
log.info(dishDto.toString());
dishService.updateWishFlavor(dishDto);
return R.success("添加菜品成功");
}
删除停售功能开发
停售和批量停售是同一请求方法
DishController表现层
/**
* 修改status状态
*
* @param status
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> updateStatus(@PathVariable Integer status, @RequestParam List<Long> ids) {
log.info("status:{}", status);
log.info("ids:{}", ids);
//根据传过来的ids数组,修改dish表的status状态
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
//selec * from dish where id in(?,?,?)
queryWrapper.in(ids != null, Dish::getId, ids);
//查询数据
List<Dish> list = dishService.list(queryWrapper);
//修改dish表的status状态
list.stream().map((item) -> {
item.setStatus(status);
//修改状态
dishService.updateById(item);
return item;
}).collect(Collectors.toList());
return R.success("修改状态成功");
}
删除和批量删除
注意,对于状态为售卖中的菜品不能删除,需要先停售,然后才能删除。
删除菜品的同时删除关联的菜品口味信息
业务层DishService接口
//删除菜品的同时删除对应的菜品口味
public void deleteWithFlavor(List<Long> ids);
业务层实现类DishServiceImpl
/**
* 删除菜品的同时删除对应的菜品口味
* @param ids
*/
@Override
@Transactional
public void deleteWithFlavor(List<Long> ids) {
//查询菜品状态,确定是否可以删除
//select count(*) from dish where id in (?,?,?) and status = 1
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Dish::getId,ids);
queryWrapper.eq(Dish::getStatus,1);
int count = this.count(queryWrapper);
if (count>0){
//不能删除,抛出一个业务异常
throw new CustomException("菜品正在售卖中,不能删除");
}
//可以删除,先删除从菜品的基本信息dish表
this.removeByIds(ids);
//删除菜品和菜品口味关联的信息 dish_flavor
//delete from dish_flavor where dish_id in (?,?,?)
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(DishFlavor::getDishId,ids);
dishFlavorService.remove(lambdaQueryWrapper);
}
控制层实现DishController
/**
* 删除菜品
*
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids) {
log.info("ids:{}", ids);
dishService.deleteWithFlavor(ids);
return R.success("删除数据成功");
}
套餐管理业务开发
新增套餐
需求分析
套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,
在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,
并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
数据模型
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,
还需要向setmeal dish表插入套餐和菜品关联数据所以在新增套餐时,涉及到两个表:
setmeal 套餐表
setmeal dish 套餐菜品关系表
代码开发
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类 SetmealDish(直接从课程资料中导入即可,Setmeal实体前面课程中已经导入过了)
DTO SetmealDto (直接从课程资料中导入即可)
Mapper接口 SetmealDishMapper
业务层接口 SetmealDishService
业务层实现类 SetmealDishServicelmpl
控制层 SetmealController
在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:
1、页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2、页面发送aiax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3、页面发送aiax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
4、页面发送请求进行图片上传,请求服务端将图片保存到服务器
5、页面发送请求进行图片下载,将上传的图片进行回显
6、点击保存按钮,发送ajax请求,将套餐相关数据以ison形式提交到服务端
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。
业务层接口
public interface SetmealService extends IService<Setmeal> {
/**
* 新增套餐,同时保存套餐和菜品的关联关系
* @param setmealDto
*/
public void saveWithDish(SetmealDto setmealDto);
}
业务层接口实现类
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
@Autowired
private SetmealDishService setmealDishService;
/**
* 新增套餐,同时保存套餐和菜品的关联关系
*
* @param setmealDto
*/
@Override
@Transactional//事务管理
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作setmeal表,执行insert操作
this.save(setmealDto);
//获取setmealDto里的SetmealDish数据
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
//遍历setmealDishes,将setmealId写入SetmealDish
setmealDishes.stream().map((item -> {
item.setSetmealId(setmealDto.getId());
return item;
})).collect(Collectors.toList());
//保存套餐和菜品的关联信息,操作setmeal_dish表,执行insert操作
setmealDishService.saveBatch(setmealDishes);//批量保存
}
}
表现层
/**
* 套餐管理
*/
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishService setmealDishService;
/**
* 新增套餐
*
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto) {
log.info("套餐信息:{}", setmealDto.toString());
setmealService.saveWithDish(setmealDto);
return R.success("添加套餐成功");
}
}
套餐信息分页查询
需求分析
系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,
所以一般的系统中都会以分页的方式来展示列表数据。
代码开发
在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:
1、页面(backend/page/combo/list.html)发送ajax请求,
将分页查询参数(page、pageSizename)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
/**
* 套餐管理分页查询
*
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
//创建分页构造器
Page<Setmeal> pageInfo = new Page<>(page, pageSize);
Page<SetmealDto> setmealDtoPage = new Page<>();
//创建条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null, Setmeal::getName, name);
//添加排序条件,根据更新时间降序排序
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
//执行查询
setmealService.page(pageInfo, queryWrapper);
//对象拷贝
//将PageInfo对象中的属性复制到dishDtoPage对象中,但是不拷贝"records"属性
BeanUtils.copyProperties(pageInfo, setmealDtoPage, "records");
//获取Setmeal的所有数据,records属性是分页查询里所有数据的集合
List<Setmeal> records = pageInfo.getRecords();
//遍历records集合,获取Setmeal的菜品分类id,根据这个id查询SetmealDto里的categoryName
List<SetmealDto> list = records.stream().map((item -> {
SetmealDto setmealDto = new SetmealDto();
//将Setmeal里的属性拷贝给setmealDto
BeanUtils.copyProperties(item, setmealDto);
//获取菜品分类id
Long categoryId = item.getCategoryId();
//根据id查询菜品分类名称
Category category = categoryService.getById(categoryId);
//将查询出来的菜品分类名插入setmealDto
if (category != null) {
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
})).collect(Collectors.toList());
setmealDtoPage.setRecords(list);
return R.success(setmealDtoPage);
}
套餐修改
回显修改页面数据
表现层代码实现
/**
* 根据id查询对应的套餐和菜品关系 回显修改页面的数据
* 请求网址: http://localhost:8080/setmeal/1664933287003922434
* 请求方法: GET
* @param id
* @return
*/
@GetMapping("/{id}")
public R<SetmealDto> get(@PathVariable Long id) {
SetmealDto setmealDto = setmealService.getByIdWishDish(id);
return R.success(setmealDto);
}
业务层接口
//根据id查询菜品信息和对应的口味信息
public SetmealDto getByIdWishDish(Long id);
业务层接口实现类
/**
* 根据id查询对应的套餐和菜品关系
* @param id
* @return
*/
@Override
@Transactional
public SetmealDto getByIdWishDish(Long id) {
//套餐信息 查询setmeal表的套餐id
Setmeal setmeal = this.getById(id);
//关联的菜品信息 查询setmeal_dish表的
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId,setmeal.getId());
List<SetmealDish> setmealDishes = setmealDishService.list(queryWrapper);
SetmealDto setmealDto = new SetmealDto();
//对象拷贝
BeanUtils.copyProperties(setmeal,setmealDto);
setmealDto.setSetmealDishes(setmealDishes);
return setmealDto;
}
修改套餐
表现层代码实现
/**
* 请求网址: http://localhost:8080/setmeal
* 请求方法: PUT
* @param setmealDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody SetmealDto setmealDto) {
log.info(setmealDto.toString());
setmealService.updateWishFlavor(setmealDto);
return R.success("添加菜品成功");
}
业务层接口实现
//修改套餐
public void updateWishFlavor(SetmealDto setmealDto);
业务层实现类
/**
* 修改套餐
*
* @param setmealDto
*/
@Override
@Transactional
public void updateWishFlavor(SetmealDto setmealDto) {
//更新dish表基本信息
this.updateById(setmealDto);
//清理套餐关联的菜品 setmeal_dish 表的delete操作
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
//查询套餐id关联的菜品数据
queryWrapper.eq(SetmealDish::getSetmealId, setmealDto.getId());
setmealDishService.remove(queryWrapper);
//添加提交过来的菜品信息 setmeal_dish 表的insert操作
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
//遍历setmealDishes集合,将套餐id添加进setmeal_dish表
setmealDishes.stream().map((item -> {
item.setSetmealId(setmealDto.getId());
return item;
})).collect(Collectors.toList());
//保存套餐菜品到setmeal_dish表
setmealDishService.saveBatch(setmealDishes);
}
停售/启售
需求分析
在套餐管理列表页面点击启售/停售按钮,可以启售/停售对应的套餐信息。
也可以通过复选框选择多个套餐,点击批量启售/启停按钮一次启售/停售多个套餐。
代码开发
/**
* 修改套餐状态
* @param status
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> update(@PathVariable("status") Integer status, @RequestParam List<Long> ids) {
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//查询需要修改的对应id数据
queryWrapper.in(Setmeal::getId, ids);
List<Setmeal> list = setmealService.list(queryWrapper);
//遍历list数组修改状态
for (Setmeal setmeal : list) {
setmeal.setStatus(status);
setmealService.updateById(setmeal);
}
return R.success("修改状态成功");
}
删除套餐
需求分析
在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。
也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。
注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
代码开发
业务层SetmealService接口添加删除套餐方法
/**
* 删除套餐,同时删除套餐和菜品的关联关系
* @param ids
*/
public void deleteWithDish(List<Long> ids);
业务层实现类SetmealServiceImpl
/**
* 删除套餐,同时删除套餐和菜品的关联关系
*
* @param ids
*/
@Override
@Transactional
public void deleteWithDish(List<Long> ids) {
//select count(*) from setmeal where id in (?,?,?) and status = 1
//查询套餐状态,确定是否可以删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId, ids);
queryWrapper.eq(Setmeal::getStatus, 1);
int count = this.count(queryWrapper);
if (count > 0) {
//不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,不能删除");
}
//如果可以,删除套餐的基本信息,操作setmeal表,执行delete操作
this.removeByIds(ids);
//删除套餐和菜品的关联信息,操作setmeal_dish表,执行delete操作
//delete from setmeal_dish where setmeal_id in (?,?,?)
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(lambdaQueryWrapper);
}
订单管理
/**
* 请求网址:http://localhost:8080/order/page?page=1&pageSize=10
* 请求方法:GET
* 请求参数:
* page: 1
* pageSize: 10
* number: 1 //订单号
* beginTime: 2023-06-19 00:00:00 //开始时间
* endTime: 2023-07-18 23:59:59 //结束时间
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String number, String beginTime, String endTime) {
log.info("page:{}, pageSize:{}, pageSize:{}, beginTime:{}, endTime:{}",
page, pageSize, number, beginTime, endTime);
//创建分页构造器
Page pageInfo = new Page(page, pageSize);
//创建条件构造器
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件 number订单号 订单号是唯一的
queryWrapper.eq(number != null, Orders::getNumber, number);
//下单时间 beginTime~endTime
//SELECT * FROM orders WHERE orderTime >= 'beginTime' AND orderTime <= 'endTime';
// queryWrapper.ge(beginTime != null, Orders::getOrderTime, beginTime);
// queryWrapper.le(endTime != null, Orders::getOrderTime, endTime);
queryWrapper.between(beginTime != null, Orders::getOrderTime, beginTime, endTime);
//添加下单时间降序排序
queryWrapper.orderByDesc(Orders::getOrderTime);
//执行查询
orderService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
移动端在登录时,没有设置用户名username,所有响应回来的数据是null
修改订单状态
/**
* 请求网址: http://localhost:8080/order
* 请求方法: PUT
* 请求参数:{
* "status": 3,
* "id": "1665575614537969666"
* }
* 修改订单状态
* @param orders
* @return
*/
@PutMapping
public R<String> put(@RequestBody Orders orders){
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Orders::getId,orders.getId());
orderService.update(orders,queryWrapper);
return R.success("修改状态成功");
}
}