本项目(瑞吉外卖
)是专门为餐饮企业(餐厅、饭店
)定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护
。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等
。
本项目供分为3期进行开发
第一期
实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问;
第二期
针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便;
第三期
针对系统进行优化升级,提高系统的访问性能;
产品原型
一款产品成型之前的一个简单框架,就是将页面的排版布局展现出现,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观的了解项目的需求和提供的功能。
注意事项:产品原型主要用于展示项目的功能,并不是最终的页面效果
后台系统管理员
后台系统普通员工
C端用户
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 | 订单明细表 |
<parent>
<artifactId>spring-boot-starter-parentartifactId>
<groupId>org.springframework.bootgroupId>
<version>2.1.4.RELEASEversion>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.1.23version>
dependency>
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.11version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>2.6.6version>
plugin>
plugins>
build>
创建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
静态资源映射
@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/");
}
}
@SpringBootApplication
@Slf4j
public class ReggieApplication {
public static void main(String[] args) {
log.info("项目启动成功!");
SpringApplication.run(ReggieApplication.class, args);
}
}
需求分析
通过访问登录页面http://localhost:8080/backend/page/login/login.html
,点击登录按钮时,页面会发送请求login
以及提交的参数username
和password
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;
}
EmployeeMapper
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
EmployeeService
public interface EmployeeService extends IService<Employee> {
}
EmployeeServiceImpl
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
由于前端页面需要后端接口返回对应的信息,所以引入R这个通用结果类;
R
/**
* 通用结果类
* @param
*/
@Data
public class R<T> {
// 状态码
private Integer code;
// 错误信息
private String msg;
// 数据
private T data;
private Map map=new HashMap();
/**
* 成功时返回
* @param object
* @param
* @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
* @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;
}
}
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);
}
}
员工登陆成功后,页面跳转到后台系统首页面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、创建自定义过滤器
2、在启动类上加入注解@ServletComponentScan
3、完善过滤器的处理逻辑
@ServletComponentScan
注解解析:
@WebServlet
注解自动注册@WebFilter
注解自动注册@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;
}
}
需求分析
后台系统中可以管理员工信息,通过新增员工信息来添加系统用户。点击【添加员工】按钮跳转到新增页面。
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将新增员工页面输入的数据以json的形式提交到服务端;
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存;
3、Service调用Mapper操作数据库,保存数据。
@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'
,只需换一个测试数据,重新输入,后面会对报错进行统一处理。
/**
* 全局异常处理
*/
@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("失败了");
}
}
功能测试
登陆后,添加一个一个已经存在账号名,看前端页面提示信息,以及看后台是否输出了报错日志;
需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将分页查询参数page、pageSize、name
提交到服务端;
2、服务端Controller接受页面提交的数据并调用Service查询数据;
3、Service调用Mappers操作数据库,查询分页数据;
4、Controller将查询到的分页数据响应给页面;
5、页面接收到分页数据并通过ElemenUI的Table组件展示到页面上。
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条数据
问题分析
解决方法
/**
* 配置mybatis-plus提供的分页插件拦截器
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
需求分析
在员工管理列表页面中,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录管理系统,启用后可以正常登录;
需要注意的是:只有管理员(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
/**
* 根据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("修改状态成功!");
}
需求分析
在员工管理列表页面点击编辑按钮,跳转到编辑页面,在标记页面辉县员工信息并进行修改,最后点击保存按钮完成编辑操作
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
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,所以只需要在前端跳转发请求就行;这样就实现了方法的复用,减少了代码量。
测试
前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段
,在编辑员工时需要设置修改时间和修改人
等字段,这些字段属于公共字段
,也就是很多表中都有这些字段,如下:
代码实现
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
需求分析
数据模型
新增分类,其实就是将我们新增窗口录入的分类数据插入到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("新增分类成功");
}
需求分析
进入分类管理页面,分页查询出来所有分类套餐
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax
请求,将分页查询参数page、pageSize
提交到服务器
2、服务器Controller
接收页面提交的数据并调用Service
查询数据
3、Service
调用Mapper
操作数据库,查询分页数据
4、Controller
将查询到的分页数据响应给页面
5、页面接收分页数据并通过ElementUI
的Table组件
展示到页面上
/**
* 分页查询分类信息
* @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);
}
/**
* 删除分类
* @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("删除分类");
}
注意:这里的删除功能是不完善的,因为可能需要删除的数据是与其他表关联的,所以删除前需要判断该条数据是否关联其他表
/**
* 菜品类
*/
@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()
方法
测试
删除湘菜时,由于关联了数据,所以不能删除并给出错误信息
/**
* 根据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("修改分类信息成功");
}
文件上传下载介绍
文件下载介绍
文件上传代码实现
启动项目,访问http://localhost:8080/backend/page/demo/upload.html
(需要先登录)
@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);
}
/**
* 文件下载
* @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();
}
}
/**
* 菜品口味
*/
@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
形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可
点击新增菜品,需要先获取菜品分类的列表
/**
* 获取菜品分类列表
* @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
表中
在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务器的交互过程:
1、页面backend/page/food/list.html
发送ajax
请求,将分页查询参数page,pageSize,name
提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端发送的这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);
}
测试
在开发代码之前,需要梳理一下修改菜品时前端页面add.html
和服务端交互过程:
1、页面发送ajax
请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送ajax
请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax
请求,将修改后的菜品相关数据以json
形式提交到服务端
开发修改菜品功能,其实就是在负端编写代码去处理前端页面发送的这4次请求
点击修改按钮,根据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);
}
页面请求
携带的参数信息
// 更新菜品信息和对应口味信息
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("修改菜品成功");
}
测试
需求分析
当我们不需要这个菜品或者这个菜品暂时没有时,我们可以选择停售这个菜品。使得用户可以清晰的了解到这个菜品停售;
代码开发
页面请求:由于单个和批量属于同一个方法,当我们实现了批量起售或停售时,也就可以实现单个起售/停售了
DishController
由于status
只设计dish
表,所以修改起来还是很简单的
/**
* 批量起售/停售
* @param status
* @param ids
* @return
*/
@RequestMapping("/status/{status}")
public R<String> updateStatus(@PathVariable("status") Integer status,@RequestParam("ids") List<Long> ids){
log.info("修改菜品状态");
// 构造条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ids!=null,Dish::getId,ids);
// 批量查询
List<Dish> dishes = dishService.list(queryWrapper);
for (Dish dish : dishes) {
if (dish!=null){
dish.setStatus(status);
dishService.updateById(dish);
}
}
return R.success("售卖状态修改成功!");
}
测试
需求分析
代码开发
由于删除操作涉及了dish
和 dish_flavor
两张表,所以我们把它写在Service层
DishService
// 删除菜品信息和对应口味信息
void deleteWithFlavor(List<Long> ids);
DishServiceImpl
/**
* 删除菜品信息以及口味信息
* @param ids
*/
@Override
@Transactional
public void deleteWithFlavor(List<Long> ids) {
// 删除菜品信息
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.in(ids!=null,Dish::getId,ids);
// 若菜品为起售状态,不可删除
dishLambdaQueryWrapper.eq(Dish::getStatus,1);
long count = this.count(dishLambdaQueryWrapper);
if (count>0){
// 如果不能删除,则抛出异常
throw new CustomException("菜品为起售状态,不可删除");
}
// 如果能删除,则删除菜品信息
this.removeByIds(ids);
// 删除对应口味信息
LambdaQueryWrapper<DishFlavor> flavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
flavorLambdaQueryWrapper.in(ids!=null,DishFlavor::getDishId,ids);
// 删除对应口味信息
dishFlavorService.remove(flavorLambdaQueryWrapper);
}
DishController
/**
* 删除/批量删除
* @param ids
* @return
*/
@RequestMapping(value = "",method = RequestMethod.DELETE)
public R<String> delete(@RequestParam("ids") List<Long> ids){
log.info("删除或批量删除菜品");
dishService.deleteWithFlavor(ids);
return R.success("删除菜品成功!");
}
测试
删除菜品为test88的菜品
由于为起售状态,所以不能删除
停售之后删除
在开发代码之前,需要梳理一下新增套餐时和服务端的交互过程:
1、页面backend/page/combo/add.html
发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2、页面发送ajax
请求,请求服务端获取菜品分类数据
并展示到添加菜品窗口
中
3、页面发送ajax
请求,请求服务端,根据菜品分类查询对应的菜品数据
并展示到添加菜品窗口
中
4、页面发送请求进行图片上传
,请求服务端将图片保存到服务器
5、页面发送请求进行图片下载
,将上传的图片进行回显
6、点击保存按钮,发送ajax
请求,将套餐相关数据以json
形式提交到服务端
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可
根据菜品分类将对应菜品信息展示到添加菜品窗口中
DishController
/**
* 根据条件查询菜品信息
* @param dish
* @return
*/
@RequestMapping(value = "/list",method = RequestMethod.GET)
public R<List<Dish>> list(Dish dish){
log.info("根据菜品id查询菜品信息");
// 构造条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
// 查询状态为 1(起售的菜品)
queryWrapper.eq(Dish::getStatus,1);
// 添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> dishes = dishService.list(queryWrapper);
return R.success(dishes);
}
效果
点击保存
按钮,发送页面请求
由于新增套餐中的setmealDishes
在setmeal
中不存在,所以需要引入SetmealDto
@Data
public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName;
}
新增套餐业务涉及了setmeal
和setmeal_dish
两张表,所以在Service层中进行判断
SetmealService
// 新增套餐,以及菜品关系信息
void saveWithDish(SetmealDto setmealDto);
SetmealServiceImpl
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
// 保存套餐基本信息
this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
// 保存套餐和菜品的关联信息
setmealDishService.saveBatch(setmealDishes);
}
SetmealController
/**
* 添加套餐
* @param setmealDto
* @return
*/
@RequestMapping(value = "",method = RequestMethod.POST)
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、pageSize、name
提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求
页面请求
SetmealController
/**
* 分页查询套餐信息
* @param page
* @param pageSize
* @param name
* @return
*/
@RequestMapping(value = "/page",method = RequestMethod.GET)
public R<Page<Setmeal>> page(int page,int pageSize,String name){
// 分页构造器
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
// 条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件,根据name进行模糊查询
queryWrapper.like(name!=null,Setmeal::getName,name);
// 添加排序条件
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
测试
代码改进
/**
* 分页查询套餐信息
* @param page
* @param pageSize
* @param name
* @return
*/
@RequestMapping(value = "/page",method = RequestMethod.GET)
public R<Page<SetmealDto>> page(int page,int pageSize,String name){
// 分页构造器
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtoPage = new Page<>(page,pageSize);
// 条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件,根据name进行模糊查询
queryWrapper.like(name!=null,Setmeal::getName,name);
// 添加排序条件
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
// 对象拷贝
BeanUtils.copyProperties(pageInfo,dtoPage);
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list=records.stream().map((item)->{
SetmealDto setmealDto = new SetmealDto();
// 对象拷贝
BeanUtils.copyProperties(item,setmealDto);
// 分类id
Long categoryId = item.getCategoryId();
// 根据分类id查询分类对象
Category category = categoryService.getById(categoryId);
if (category!=null){
// 分类名称
String categoryName=category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
由于套餐信息涉及setmeal
和 setmeal_dish
两张表,所以在Service层进行判断
SetmealService
// 删除套餐,以及菜品关系信息
void deleteWithDish(List<Long> ids);
SetmealServiceImpl
/**
* 删除套餐,以及菜品关系信息
* @param ids
*/
@Override
@Transactional
public void deleteWithDish(List<Long> ids) {
// 1.删除套餐表中信息
LambdaQueryWrapper<Setmeal> setmealQueryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件
setmealQueryWrapper.in(ids != null, Setmeal::getId, ids);
// 是否起售
setmealQueryWrapper.eq(Setmeal::getStatus, 1);
// 查询删除中起售套餐的数量
long count = this.count(setmealQueryWrapper);
if (count > 0) {
// 如果删除的套餐中为起售状态,不能删除
throw new CustomException("起售中的套餐不能删除");
}
// 删除套餐表中的数据
this.removeByIds(ids);
// 2.删除套餐菜品信息表中数据
LambdaQueryWrapper<SetmealDish> setmealDishQueryWrapper = new LambdaQueryWrapper<>();
setmealDishQueryWrapper.in(ids != null, SetmealDish::getSetmealId, ids);
setmealDishService.remove(setmealDishQueryWrapper);
}
SetmealController
/**
* 删除与批量删除套餐
* @param ids
* @return
*/
@RequestMapping(value = "",method = RequestMethod.DELETE)
public R<String> delete(@RequestParam("ids") List<Long> ids){
log.info("删除与批量删除套餐");
setmealService.deleteWithDish(ids);
return R.success("删除套餐信息成功!");
}
测试
需求分析
在套餐管理列表页面点击停售按钮,可以停售对应的套餐。也可以通过复选框选择多个套餐,点击批量停售按钮一次停售多个套餐。
页面请求
代码开发
SetmealController
/**
* 修改售卖状态
* @param status
* @param ids
* @return
*/
@RequestMapping(value = "/status/{status}",method = RequestMethod.POST)
public R<String> update(@PathVariable("status") Integer status,@RequestParam("ids") List<Long> ids){
log.info("停售与批量停售套餐");
// 条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ids!=null,Setmeal::getId,ids);
List<Setmeal> list = setmealService.list(queryWrapper);
for (Setmeal setmeal : list) {
if (setmeal!=null){
setmeal.setStatus(status);
setmealService.updateById(setmeal);
}
}
return R.success("修改状态成功");
}
需求分析
在套餐管理列表页面点击修改按钮,可以修改对应的套餐。
修改套餐之前,我们需要根据id获取套餐的信息来进行数据回显。
页面请求
/**
* 根据id获取套餐信息
* @param id
* @return
*/
@RequestMapping(value = "/{id}",method = RequestMethod.GET)
public R<SetmealDto> getById(@PathVariable("id") Long id){
log.info("根据id:{}获取套餐信息",id);
// 1.获取套餐信息
Setmeal setmeal = setmealService.getById(id);
// 将套餐信息复制到dto中
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(setmeal,setmealDto);
// 2.获取套餐菜品信息
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId,id);
List<SetmealDish> dishes = setmealDishService.list(queryWrapper);
setmealDto.setSetmealDishes(dishes);
return R.success(setmealDto);
}
效果
点击修改
按钮,根据套餐id,页面进行数据的回显
修改套餐信息
页面请求
套餐的信息涉及setmeal
和 setmeal_dish
两张表
SetmealService
// 修改套餐,以及菜品关系信息
void updateWithDish(SetmealDto setmealDto);
SetmealServiceImpl
/**
* 更新信息(含菜品信息)
* @param setmealDto
*/
@Override
@Transactional
public void updateWithDish(SetmealDto setmealDto) {
// 1.更新套餐信息(setmeal表)
this.updateById(setmealDto);
// 清除当前菜品关系信息
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId,setmealDto.getId());
setmealDishService.remove(queryWrapper);
// 添加菜品关系信息
List<SetmealDish> dishes = setmealDto.getSetmealDishes();
dishes=dishes.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
setmealDishService.saveBatch(dishes);
}
SetmealController
/**
* 修改套餐信息
* @param setmealDto
* @return
*/
@RequestMapping(value = "",method = RequestMethod.PUT)
public R<String> update(@RequestBody SetmealDto setmealDto){
log.info("setmealDto:{}",setmealDto.toString());
setmealService.updateWithDish(setmealDto);
return R.success("修改成功!");
}
Short Message Service 是广大企业客户快速触达手机用户所优选使用的通信能力;调用API或群发助手,即可发送验证码、通知类和营销类短信;
国内验证短信秒级触达,到达率可达99%;国际/港澳短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。
应用场景
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
<version>3.2.6version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-cmsartifactId>
<version>7.0.24version>
dependency>
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}
}
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);
/*SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}*/
}
}
ValidateCodeUtils
/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}
/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
若前端页面点击获取验证码,验证码框自动呈现验证码,则休要修改前端页面front/page/login.html
在front/api/login.js
中添加sendMsgApi
效果
点击获取验证码,发起sendMsg
请求
UserController
/**
* 获取验证码
* @param user
* @param session
* @return
*/
@RequestMapping(value = "/sendMsg", method = RequestMethod.POST)
public R<String> sendMsg(@RequestBody User user,HttpSession session) {
// 获取手机号
String phone = user.getPhone();
if (StringUtils.isNotEmpty(phone)){
// 生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
log.info("code:{}",code);
// 调用阿里云提供的短信服务API完成发送短信
//SMSUtils.sendMessage("瑞吉外卖","",phone,code);//仅供测试
// 需要将生成的验证码保存到Session
session.setAttribute(phone,code);
return R.success("手机验证码短信发送成功");
}
return R.error("短信发送失败");
}
效果
页面请求
如果页面只传入phone
则需要修改前端页面front/page/login.html
效果
UserController
/**
* 移动端用户登录
* @param map
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
public R<User> login(@RequestBody Map map, HttpSession session){
log.info(map.toString());
// 获取手机号
String phone = map.get("phone").toString();
// 获取验证码
String code = map.get("code").toString();
// 从Session中获取保存的验证码
Object codeInSession = session.getAttribute(phone);
// 进行验证码比对
if (codeInSession!=null&&codeInSession.equals(code)){
// 如果能够比对成功,说明登录成功
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone,phone);
// 查询当前用户
User user = userService.getOne(queryWrapper);
// 判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
if (user == null) {
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
return R.success(user);
}
return R.error("登陆失败");
}
注意:将userId放入Session
测试
/**
* 退出功能
* @param request
* @return
*/
@RequestMapping("/loginout")
public R<String> logout(HttpServletRequest request){
// 清楚session中的phone
request.getSession().removeAttribute("user");
return R.success("退出成功!");
}
当我们输入要添加的用户信息,点击保存时,页面发送ajax请求
AddressBookController
/**
* 新增收货地址
* @param addressBook
* @return
*/
@RequestMapping(value = "", method = RequestMethod.POST)
public R<AddressBook> save(@RequestBody AddressBook addressBook) {
log.info(addressBook.toString());
// 获取当前userId
Long userId = BaseContext.getCurrentId();
addressBook.setUserId(userId);
addressBookService.save(addressBook);
return R.success(addressBook);
}
测试
/**
* 查询所有收货地址信息
* @return
*/
@RequestMapping(value = "/list",method = RequestMethod.GET)
public R<List<AddressBook>> list(){
log.info("查询所有收货地址信息");
// 构造条件构造器
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
// 添加条件
queryWrapper.eq(AddressBook::getUserId,BaseContext.getCurrentId());
// 排序条件
queryWrapper.orderByDesc(AddressBook::getUpdateTime);
List<AddressBook> list = addressBookService.list(queryWrapper);
return R.success(list);
}
效果
页面请求
/**
* 修改默认地址
* @param addressBook
* @return
*/
@RequestMapping(value = "/default",method = RequestMethod.PUT)
public R<AddressBook> setDefault(@RequestBody AddressBook addressBook){
log.info("修改地址簿默认地址");
// 条件构造器
LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(AddressBook::getUserId,BaseContext.getCurrentId());
// 将所有的默认字段设置为 0(没有一个默认字段)
wrapper.set(AddressBook::getIsDefault,0);
// SQL: update address_book set is_default = 0 where user_id = ?
addressBookService.update(wrapper);
addressBook.setIsDefault(1);
// SQL: update address_book set is_default =1 where id = ?
addressBookService.updateById(addressBook);
return R.success(addressBook);
}
效果
当我们点击设为默认地址
,即可切换当前的默认地址
点击按钮,进行修改默认地址信息。但修改之前,我们需要根据 id 获取地址信息来进行修改页面的数据回显
根据id获取地址信息
/**
* 根据id获取地址簿信息
* @param id
* @return
*/
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public R<AddressBook> getById(@PathVariable("id") Long id) {
log.info("获取id为:{}的地址簿信息", id);
// 条件构造器
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
// 添加查询条件
queryWrapper.eq(AddressBook::getId, id);
// 查询对象
AddressBook addressBook = addressBookService.getOne(queryWrapper);
if (addressBook != null) {
return R.success(addressBook);
}
return R.error("没有找到对象");
}
效果
我们发现,数据回显的页面性别未被选中,而在响应的数据中也并未出现问题
由于前端页面sex
属性用的是字符串,而我们后端数据库是int
类型的 所以不匹配,只需修改前端页面sex
属性,或者修改数据库即可
重新Build Project即可生效
修改地址簿
/**
* 修改地址簿信息
* @param addressBook
* @return
*/
@RequestMapping(value = "",method = RequestMethod.PUT)
public R<String> update(@RequestBody AddressBook addressBook){
log.info("修改地址簿信息:{}",addressBook);
addressBookService.updateById(addressBook);
return R.success("地址簿信息修改成功");
}
测试
front/page/address.html
修改之后的效果
{"code": 1,"msg": null,"data": [],"map": {}}
/**
* 根据条件查询菜品信息
*
* @param dish
* @return
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
public R<List<DishDto>> list(Dish dish) {
log.info("根据菜品id查询菜品信息");
// 构造条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
// 查询状态为 1(起售的菜品)
queryWrapper.eq(Dish::getStatus, 1);
// 添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
List<DishDto> dishDtoList = list.stream().map((item) -> {
// 创建dishDto对象
DishDto dishDto = new DishDto();
// 拷贝对象
BeanUtils.copyProperties(item, dishDto);
Long categoryId = item.getCategoryId();
// 根据id查询分类对象 获取分类名字
Category category = categoryService.getById(categoryId);
if (category != null) {
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
// 当前菜品id
Long dishId = item.getId();
// 获取口味信息
LambdaQueryWrapper<DishFlavor> flavorQueryWrapper = new LambdaQueryWrapper<>();
flavorQueryWrapper.eq(DishFlavor::getDishId, dishId);
List<DishFlavor> dishFlavorList = dishFlavorService.list(flavorQueryWrapper);
// 设置口味信息
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
页面展示效果
/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@RequestMapping(value = "/list",method = RequestMethod.GET)
public R<List<Setmeal>> list(@RequestBody Setmeal setmeal){
//条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
// 添加条件 (分类id 和 售卖状态)
queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus());
// 添加排序条件
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(queryWrapper);
return R.success(list);
}
测试
需求分析
数据模型
/**
* 添加购物车
* @param shoppingCart
* @return
*/
@RequestMapping(value = "/add", method = RequestMethod.POST)
public R<ShoppingCart> save(@RequestBody ShoppingCart shoppingCart) {
log.info("购物车数据:{}", shoppingCart);
// 设置用户id,指定当前是那个用户的购物车数据
shoppingCart.setUserId(BaseContext.getCurrentId());
// 查询当前菜品或套餐是否在购物车中
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
if (dishId != null) {
// 添加的是菜品
queryWrapper.eq(ShoppingCart::getDishId, dishId);
} else {
// 添加的是套餐
queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
// SQL: select * from shopping_cart where user_id = ? and dish_id/setmeal_id =?
ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
if (cartServiceOne != null) {
// 如果已经存在,则在原来的基础上加 1
Integer number = cartServiceOne.getNumber();
cartServiceOne.setNumber(number + 1);
shoppingCartService.updateById(cartServiceOne);
} else {
// 如果不存在,则添加到购物车,数量默认 为 1
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartService.save(shoppingCart);
cartServiceOne = shoppingCart;
}
return R.success(cartServiceOne);
}
/**
* 查看购物车
* @return
*/
@RequestMapping(value = "/list",method = RequestMethod.GET)
public R<List<ShoppingCart>> list(){
log.info("查看购物车...");
// 条件构造器
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
// 添加排序条件
queryWrapper.orderByDesc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
return R.success(list);
}
页面请求
/**
* 清空购物车
* @return
*/
@RequestMapping(value = "/clean",method = RequestMethod.DELETE)
public R<String> clean(){
// SQL:delete from shopping_cart where user_id = ?
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
shoppingCartService.remove(queryWrapper);
return R.success("清空购物车成功");
}
需求分析
数据模型
/**
* 获取默认地址簿信息
* @return
*/
@RequestMapping(value = "/default", method = RequestMethod.GET)
public R<AddressBook> queryDefault() {
// 条件构造器
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件
queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
queryWrapper.eq(AddressBook::getIsDefault, 1);
// 执行查询
AddressBook addressBook = addressBookService.getOne(queryWrapper);
if (addressBook != null) {
return R.success(addressBook);
}
return R.error("没有找到该对象");
}
效果
点击去支付,发送下单的ajax请求
页面只提供了`addressBookId、payMethod、remark``地址簿id,支付方式和备注,因为当前用户的id后台已经获取到,可以通过用户id查询购物车的信息。
OrdersService
// 用户下单
void submit(Orders orders);
OrderServiceImpl
/**
* 用户下单
*
* @param orders
*/
@Override
@Transactional
public void submit(Orders orders) {
// 获取当前用户id
Long userId = BaseContext.getCurrentId();
// 查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ShoppingCart::getUserId, userId);
List<ShoppingCart> shoppingCarts = shoppingCartService.list(lambdaQueryWrapper);
// 判断购物车是否有数据
if (shoppingCarts == null || shoppingCarts.size() == 0) {
throw new CustomException("购物车数据为空,不能下单");
}
// 查询用户数据
User user = userService.getById(userId);
// 查询地址数据
Long addressBookId = orders.getAddressBookId(); //获取地址簿id
AddressBook addressBook = addressBookService.getById(addressBookId);//查询地址簿信息
if (addressBook == null) {
throw new CustomException("用户地址信息有误,不能下单");
}
long orderId = IdWorker.getId();// 订单号
// 初始化总金额 amount
AtomicInteger amount = new AtomicInteger(0);
List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
// 创建订单明细对象
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
// 向订单表插入数据(一条数据)
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));
orders.setUserId(userId);
orders.setNumber(String.valueOf(orderId));
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setPhone(addressBook.getPhone());
orders.setAddress(
(addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())
+ (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
+ (addressBook.getDetail() == null ? "" : addressBook.getDetail())
);
this.save(orders);
// 向订单明细表插入数据(多条数据)
orderDetailService.saveBatch(orderDetails);
// 清空购物车数据
shoppingCartService.remove(lambdaQueryWrapper);
}
OrderServiceController
/**
* 下单功能
* @param orders
* @return
*/
@RequestMapping(value = "/submit",method = RequestMethod.POST)
public R<String> submit(@RequestBody Orders orders){
log.info("订单信息"+orders.toString());
ordersService.submit(orders);
return R.success("下单成功!");
}
页面请求
由于前端页面展示的数据需要order
和order_detail
两张表,所以我们需要引入OrdersDto
OrdersDto
@Data
public class OrdersDto extends Orders {
private String userName;
private String phone;
private String address;
private String consignee;
private List<OrderDetail> orderDetails;
}
OrdersService
// 分页查询订单
Page<OrdersDto> page(int page, int pageSize);
OrdersServiceImpl
/**
* 分页查询订单
*
* @param page
* @param pageSize
* @return
*/
@Override
public Page<OrdersDto> page(int page, int pageSize) {
// 分页构造器
Page<Orders> pageInfo = new Page<>();
Page<OrdersDto> pageDto = new Page<>();
// 构造条件查询对象
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Orders::getUserId, BaseContext.getCurrentId());
// 添加排序条件
queryWrapper.orderByDesc(Orders::getOrderTime);
this.page(pageInfo, queryWrapper);
// 对orderDto进行赋值
List<Orders> records = pageInfo.getRecords();
List<OrdersDto> ordersDtos = records.stream().map((item) -> {
OrdersDto ordersDto = new OrdersDto();
// 拷贝
BeanUtils.copyProperties(item, ordersDto);
// 对ordersDto进行赋值
Long orderId = item.getId();// 订单id
// 通过orderId查询orderDetail
LambdaQueryWrapper<OrderDetail> detailLambdaQueryWrapper = new LambdaQueryWrapper<>();
detailLambdaQueryWrapper.eq(OrderDetail::getOrderId, orderId);
List<OrderDetail> orderDetails = orderDetailService.list(detailLambdaQueryWrapper);
// 设置OrderDetails属性
ordersDto.setOrderDetails(orderDetails);
return ordersDto;
}).collect(Collectors.toList());
// 除了 records 其余拷贝
BeanUtils.copyProperties(pageInfo, pageDto, "records");
// 设置 records
pageDto.setRecords(ordersDtos);
return pageDto;
}
OrdersController
/**
* 分页查询订单信息
* @param page
* @param pageSize
* @return
*/
@RequestMapping(value = "/userPage",method = RequestMethod.GET)
public R<Page<OrdersDto>> page(int page, int pageSize){
Page<OrdersDto> dtoPage = ordersService.page(page, pageSize);
return R.success(dtoPage);
}
效果
需求分析
当我们订单完成后,可以点击再来一单
按钮进行下单,方便快捷
页面请求
/**
* 再来一单
*
* @param map
* @return
*/
@RequestMapping(value = "/again", method = RequestMethod.POST)
public R<String> again(@RequestBody Map<String, String> map) {
log.info("map:{}", map);
// 获取订单id
String id = map.get("id");
long orderId = Long.parseLong(id);
// 条件构造器
LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(OrderDetail::getOrderId, orderId);
// 获取订单详细信息
List<OrderDetail> list = orderDetailService.list(queryWrapper);
// 清空购物车
shoppingCartService.clean();
// 获取当前用户id
Long userId = BaseContext.getCurrentId();
List<ShoppingCart> shoppingCarts = list.stream().map((item) -> {
ShoppingCart shoppingCart = new ShoppingCart();
// 把订单数据赋值给购物车对象
shoppingCart.setUserId(userId);
shoppingCart.setName(item.getName());
shoppingCart.setImage(item.getImage());
Long dishId = item.getDishId();// 菜品id
if (dishId != null) {
// 如果是菜品就添加菜品id
shoppingCart.setDishId(dishId);
} else {
// 否则就添加套餐id
shoppingCart.setSetmealId(item.getSetmealId());
}
shoppingCart.setDishFlavor(item.getDishFlavor());
shoppingCart.setNumber(item.getNumber());
shoppingCart.setAmount(item.getAmount());
shoppingCart.setCreateTime(LocalDateTime.now());
return shoppingCart;
}).collect(Collectors.toList());
// 把携带数据的购物车批量插入购物车表
shoppingCartService.saveBatch(shoppingCarts);
return R.success("操作成功");
}
测试